mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
feat(misc): give every /gacha option an equal chance
The rarity tag now only styles the wish animation and the result text, so /gacha picks like /random.
This commit is contained in:
1 parent
070f303b35
commit
4816f71fe3
3 files changed
+6
-88
No files matched your search
@@ -88,9 +88,9 @@ reply is immediate.
|
||||
`/gacha` uses the same service and token: the bot swaps the URL's last path
|
||||
segment, so `.../api/gif` becomes `.../api/gacha`. It renders a 7-second
|
||||
`640x360` silent MP4 wish animation, posts `Wishing...` while it renders, and
|
||||
falls back to a text reply such as `★★★★★ Pizza` on the same failures. Options
|
||||
tagged `*4` or `*5` are rarer, following Genshin base rates (5★ 0.6%, 4★ 5.1%,
|
||||
3★ 94.3%) over the tiers present; untagged options are 3★.
|
||||
falls back to a text reply such as `★★★★★ Pizza` on the same failures. Every
|
||||
option is equally likely, as with `/random`; a `*4` or `*5` tag only sets the
|
||||
rarity shown in the animation and reply. Untagged options are 3★.
|
||||
|
||||
## 1. MongoDB Atlas (M0)
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@ const (
|
||||
gachaDefaultRarity = gachaMinRarity
|
||||
)
|
||||
|
||||
// gachaTierRates are Genshin Impact's base single-wish odds per mille. A
|
||||
// pick first rolls a tier among the tiers present, then an option uniformly
|
||||
// within it, so untagged input is a plain uniform pick.
|
||||
var gachaTierRates = map[int]int{5: 6, 4: 51, 3: 943}
|
||||
|
||||
// gachaRarityTag matches a trailing "*3", "*4", or "*5" rarity tag.
|
||||
var gachaRarityTag = regexp.MustCompile(`\s*\*\s*([345])$`)
|
||||
|
||||
@@ -58,35 +53,6 @@ func parseGachaOptions(arg string) []gachaOption {
|
||||
return out
|
||||
}
|
||||
|
||||
// pickGachaOption returns the index of the wished option. rng may be nil to
|
||||
// use the global source.
|
||||
func pickGachaOption(options []gachaOption, rng *rand.Rand) int {
|
||||
intN := rand.IntN
|
||||
if rng != nil {
|
||||
intN = rng.IntN
|
||||
}
|
||||
byRarity := map[int][]int{}
|
||||
total := 0
|
||||
for i, option := range options {
|
||||
if len(byRarity[option.Rarity]) == 0 {
|
||||
total += gachaTierRates[option.Rarity]
|
||||
}
|
||||
byRarity[option.Rarity] = append(byRarity[option.Rarity], i)
|
||||
}
|
||||
roll := intN(total)
|
||||
for rarity := gachaMaxRarity; rarity >= gachaMinRarity; rarity-- {
|
||||
tier := byRarity[rarity]
|
||||
if len(tier) == 0 {
|
||||
continue
|
||||
}
|
||||
if roll < gachaTierRates[rarity] {
|
||||
return tier[intN(len(tier))]
|
||||
}
|
||||
roll -= gachaTierRates[rarity]
|
||||
}
|
||||
return 0 // unreachable: roll < total, the sum of present tier rates.
|
||||
}
|
||||
|
||||
// gachaResultText renders an option as its stars followed by its label.
|
||||
func gachaResultText(option gachaOption) string {
|
||||
return strings.Repeat("★", option.Rarity) + " " + option.Label
|
||||
@@ -106,7 +72,9 @@ func gachaCommand() modules.Command {
|
||||
if len(options) == 0 {
|
||||
return chathelper.Reply(ctx, b, update.Message, gachaUsage)
|
||||
}
|
||||
winner := pickGachaOption(options, nil)
|
||||
// Every option is equally likely; the rarity tag only styles the
|
||||
// wish animation and the result text.
|
||||
winner := rand.IntN(len(options))
|
||||
results := make([]string, len(options))
|
||||
for i, option := range options {
|
||||
results[i] = gachaResultText(option)
|
||||
|
||||
@@ -3,7 +3,6 @@ package misc
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
@@ -29,55 +28,6 @@ func TestParseGachaOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// One option per tier must land near the Genshin base rates.
|
||||
func TestPickGachaOption_FollowsTierRates(t *testing.T) {
|
||||
options := []gachaOption{{"five", 5}, {"four", 4}, {"three", 3}}
|
||||
rng := rand.New(rand.NewPCG(1, 2))
|
||||
const rolls = 200_000
|
||||
counts := make([]int, len(options))
|
||||
for range rolls {
|
||||
counts[pickGachaOption(options, rng)]++
|
||||
}
|
||||
for i, wantPerMille := range []float64{6, 51, 943} {
|
||||
got := float64(counts[i]) / rolls * 1000
|
||||
if got < wantPerMille*0.85 || got > wantPerMille*1.15 {
|
||||
t.Errorf("%s rate = %.2f‰, want about %.0f‰", options[i].Label, got, wantPerMille)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickGachaOption_UniformWithinTier(t *testing.T) {
|
||||
options := []gachaOption{{"a", 3}, {"b", 3}, {"c", 3}, {"d", 3}}
|
||||
rng := rand.New(rand.NewPCG(3, 4))
|
||||
counts := make([]int, len(options))
|
||||
for range 40_000 {
|
||||
counts[pickGachaOption(options, rng)]++
|
||||
}
|
||||
for i, n := range counts {
|
||||
if n < 9_000 || n > 11_000 {
|
||||
t.Errorf("option %d picked %d times, want about 10000", i, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Missing tiers do not dilute the odds: with only 4★ and 5★ options, the
|
||||
// 4★ share is 51/(51+6).
|
||||
func TestPickGachaOption_RenormalisesPresentTiers(t *testing.T) {
|
||||
options := []gachaOption{{"five", 5}, {"four", 4}}
|
||||
rng := rand.New(rand.NewPCG(5, 6))
|
||||
five := 0
|
||||
const rolls = 100_000
|
||||
for range rolls {
|
||||
if pickGachaOption(options, rng) == 0 {
|
||||
five++
|
||||
}
|
||||
}
|
||||
got := float64(five) / rolls
|
||||
if want := 6.0 / 57; got < want*0.9 || got > want*1.1 {
|
||||
t.Fatalf("5★ share = %.4f, want about %.4f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGacha_EmptyArgsRepliesUsage(t *testing.T) {
|
||||
for _, text := range []string{"/gacha", "/gacha , ,", "/gacha *5"} {
|
||||
t.Run(text, func(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user