Compare commits

...

2 Commits

Author SHA1 Message Date
c00d3d0186 cmd/horsebot: move to here 2026-02-10 14:03:41 -05:00
a534975601 horsegen: generate alternate races/saddles with ids 2026-02-10 13:55:47 -05:00
17 changed files with 960 additions and 396 deletions

1
cmd/horsebot/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
token

10
cmd/horsebot/README.md Normal file
View File

@@ -0,0 +1,10 @@
# horsebot
Discord bot serving horse game data.
Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations.
## Running
The bot always uses the Gateway API.
If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided.

View File

@@ -0,0 +1,56 @@
package autocomplete
import (
"bytes"
"cmp"
"slices"
"sync"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
)
// Set is an autocomplete set.
type Set[V any] struct {
keys []util.Chars
vals []V
}
// Add associates a value with a key in the autocomplete set.
// The behavior is undefined if the key already has a value.
func (s *Set[V]) Add(key string, val V) {
k := util.ToChars([]byte(key))
i, _ := slices.BinarySearchFunc(s.keys, k, func(a, b util.Chars) int {
return bytes.Compare(a.Bytes(), b.Bytes())
})
s.keys = slices.Insert(s.keys, i, k)
s.vals = slices.Insert(s.vals, i, val)
}
// Find appends to r all values in the set with keys that key matches.
func (s *Set[V]) Find(r []V, key string) []V {
initFzf()
var (
p = []rune(key)
got []V
t []algo.Result
slab util.Slab
)
for i := range s.keys {
res, _ := algo.FuzzyMatchV2(false, true, true, &s.keys[i], p, false, &slab)
if res.Score <= 0 {
continue
}
j, _ := slices.BinarySearchFunc(t, res, func(a, b algo.Result) int { return -cmp.Compare(a.Score, b.Score) })
// Insert after all other matches with the same score for stability.
for j < len(t) && t[j].Score == res.Score {
j++
}
t = slices.Insert(t, j, res)
got = slices.Insert(got, j, s.vals[i])
}
return append(r, got...)
}
var initFzf = sync.OnceFunc(func() { algo.Init("default") })

View File

@@ -0,0 +1,70 @@
package autocomplete_test
import (
"slices"
"testing"
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
)
func these(s ...string) []string { return s }
func TestAutocomplete(t *testing.T) {
cases := []struct {
name string
add []string
search string
want []string
}{
{
name: "empty",
add: nil,
search: "",
want: nil,
},
{
name: "exact",
add: these("bocchi"),
search: "bocchi",
want: these("bocchi"),
},
{
name: "extra",
add: these("bocchi", "ryo", "nijika", "kita"),
search: "bocchi",
want: these("bocchi"),
},
{
name: "short",
add: these("bocchi", "ryo", "nijika", "kita"),
search: "o",
want: these("bocchi", "ryo"),
},
{
name: "unrelated",
add: these("bocchi", "ryo", "nijika", "kita"),
search: "x",
want: nil,
},
{
name: "map",
add: these("Corazón ☆ Ardiente"),
search: "corazo",
want: these("Corazón ☆ Ardiente"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var set autocomplete.Set[string]
for _, s := range c.add {
set.Add(s, s)
}
got := set.Find(nil, c.search)
slices.Sort(c.want)
slices.Sort(got)
if !slices.Equal(c.want, got) {
t.Errorf("wrong results: want %q, got %q", c.want, got)
}
})
}
}

55
cmd/horsebot/log.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"log/slog"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/handler"
)
func logMiddleware(next handler.Handler) handler.Handler {
return func(e *handler.InteractionEvent) error {
var msg string
attrs := make([]slog.Attr, 0, 8)
attrs = append(attrs,
slog.Uint64("interaction", uint64(e.Interaction.ID())),
slog.Uint64("user", uint64(e.Interaction.User().ID)),
)
if guild := e.Interaction.GuildID(); guild != nil {
attrs = append(attrs, slog.String("guild", guild.String()))
}
switch i := e.Interaction.(type) {
case discord.ApplicationCommandInteraction:
msg = "command"
attrs = append(attrs,
slog.String("name", i.Data.CommandName()),
slog.Int("type", int(i.Data.Type())),
)
switch data := i.Data.(type) {
case discord.SlashCommandInteractionData:
attrs = append(attrs, slog.String("path", data.CommandPath()))
}
case discord.AutocompleteInteraction:
msg = "autocomplete"
attrs = append(attrs,
slog.String("name", i.Data.CommandName),
slog.String("path", i.Data.CommandPath()),
slog.String("focus", i.Data.Focused().Name),
)
case discord.ComponentInteraction:
msg = "component"
attrs = append(attrs,
slog.Int("type", int(i.Data.Type())),
slog.String("custom", i.Data.CustomID()),
)
default:
slog.WarnContext(e.Ctx, "unknown interaction", slog.Any("event", e))
return nil
}
slog.LogAttrs(e.Ctx, slog.LevelInfo, msg, attrs...)
return next(e)
}
}

183
cmd/horsebot/main.go Normal file
View File

@@ -0,0 +1,183 @@
package main
import (
"bytes"
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"strconv"
"time"
"github.com/disgoorg/disgo"
"github.com/disgoorg/disgo/bot"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/handler"
"github.com/disgoorg/disgo/handler/middleware"
"github.com/disgoorg/disgo/httpserver"
"github.com/disgoorg/disgo/rest"
"git.sunturtle.xyz/zephyr/horse/horse"
"git.sunturtle.xyz/zephyr/horse/horse/global"
)
func main() {
var (
tokenFile string
// http api options
addr string
route string
pubkey string
// logging options
level slog.Level
textfmt string
)
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
flag.StringVar(&addr, "http", "", "`address` to bind HTTP API server")
flag.StringVar(&route, "route", "/interactions/callback", "`path` to serve HTTP API calls")
flag.StringVar(&pubkey, "key", "", "Discord public key")
flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`")
flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json")
flag.Parse()
var lh slog.Handler
switch textfmt {
case "text":
lh = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})
case "json":
lh = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
default:
fmt.Fprintf(os.Stderr, "invalid log format %q, must be text or json", textfmt)
os.Exit(1)
}
slog.SetDefault(slog.New(lh))
token, err := os.ReadFile(tokenFile)
if err != nil {
slog.Error("reading token", slog.Any("err", err))
os.Exit(1)
}
token = bytes.TrimSuffix(token, []byte{'\n'})
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
r := handler.New()
r.DefaultContext(func() context.Context { return ctx })
r.Use(middleware.Go)
r.Use(logMiddleware)
r.Route("/skill", func(r handler.Router) {
r.SlashCommand("/", skillHandler)
r.Autocomplete("/", skillAutocomplete)
r.ButtonComponent("/{id}", skillButton)
})
opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)}
if addr != "" {
if pubkey == "" {
slog.Error("Discord public key must be provided when using HTTP API")
os.Exit(1)
}
opts = append(opts, bot.WithHTTPServerConfigOpts(pubkey,
httpserver.WithAddress(addr),
httpserver.WithURL(route),
))
}
slog.Info("connect", slog.String("disgo", disgo.Version))
client, err := disgo.New(string(token), opts...)
if err != nil {
slog.Error("building bot", slog.Any("err", err))
os.Exit(1)
}
if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil {
slog.Error("syncing commands", slog.Any("err", err))
os.Exit(1)
}
if addr != "" {
slog.Info("start HTTP server", slog.String("address", addr), slog.String("route", route))
if err := client.OpenHTTPServer(); err != nil {
slog.Error("starting HTTP server", slog.Any("err", err))
stop()
}
}
slog.Info("start gateway")
if err := client.OpenGateway(ctx); err != nil {
slog.Error("starting gateway", slog.Any("err", err))
stop()
}
slog.Info("ready")
<-ctx.Done()
stop()
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
defer stop()
client.Close(ctx)
}
var commands = []discord.ApplicationCommandCreate{
discord.SlashCommandCreate{
Name: "skill",
Description: "Umamusume skill data",
Options: []discord.ApplicationCommandOption{
discord.ApplicationCommandOptionString{
Name: "query",
Description: "Skill name or ID",
Required: true,
Autocomplete: true,
},
},
},
}
func skillHandler(data discord.SlashCommandInteractionData, e *handler.CommandEvent) error {
q := data.String("query")
id, err := strconv.ParseInt(q, 10, 32)
if err == nil {
// note inverted condition; this is when we have an id
id = int64(global.AllSkills[horse.SkillID(id)].ID)
}
if id == 0 {
// Either we weren't given a number or the number doesn't match any skill ID.
v := global.SkillNameToID[q]
if v == 0 {
// No such skill.
m := discord.MessageCreate{
Content: "No such skill.",
Flags: discord.MessageFlagEphemeral,
}
return e.CreateMessage(m)
}
id = int64(v)
}
// TODO(zeph): search conditions and effects, give a list
m := discord.MessageCreate{
Components: []discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)},
Flags: discord.MessageFlagIsComponentsV2,
}
return e.CreateMessage(m)
}
func skillAutocomplete(e *handler.AutocompleteEvent) error {
q := e.Data.String("query")
opts := skillGlobalAuto().Find(nil, q)
return e.AutocompleteResult(opts[:min(len(opts), 25)])
}
func skillButton(data discord.ButtonInteractionData, e *handler.ComponentEvent) error {
id, err := strconv.ParseInt(e.Vars["id"], 10, 32)
if err != nil {
m := discord.MessageCreate{
Content: "That button produced an invalid skill ID. That's not supposed to happen.",
Flags: discord.MessageFlagEphemeral,
}
return e.CreateMessage(m)
}
m := discord.MessageUpdate{
Components: &[]discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)},
}
return e.UpdateMessage(m)
}

148
cmd/horsebot/skill.go Normal file
View File

@@ -0,0 +1,148 @@
package main
import (
"fmt"
"strings"
"sync"
"github.com/disgoorg/disgo/discord"
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
"git.sunturtle.xyz/zephyr/horse/horse"
"git.sunturtle.xyz/zephyr/horse/horse/global"
)
func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map[int32][4]horse.SkillID) discord.ContainerComponent {
s, ok := all[id]
if !ok {
return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to RenderSkill", id))
}
thumburl := fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", s.IconID)
top := "## " + s.Name
if s.UniqueOwner != "" {
top += "\n-# " + s.UniqueOwner
}
r := discord.NewContainer(
discord.NewSection(
discord.NewTextDisplay(top),
discord.NewTextDisplay(s.Description),
).WithAccessory(discord.NewThumbnail(thumburl)),
)
var skilltype string
switch {
case s.Rarity == 3, s.Rarity == 4, s.Rarity == 5:
// unique of various star levels
r.AccentColor = 0xaca4d4
skilltype = "Unique Skill"
case s.UniqueOwner != "":
r.AccentColor = 0xcccccc
skilltype = "Inherited Unique"
case s.Rarity == 2:
// rare (gold)
r.AccentColor = 0xd7c25b
skilltype = "Rare Skill"
case s.GroupRate == -1:
// negative (purple) skill
r.AccentColor = 0x9151d4
skilltype = "Negative Skill"
case !s.WitCheck:
// should be passive (green)
r.AccentColor = 0x66ae1c
skilltype = "Passive Skill"
case isDebuff(s):
// debuff (red)
r.AccentColor = 0xe34747
skilltype = "Debuff Skill"
case s.Rarity == 1:
// common (white)
r.AccentColor = 0xcccccc
skilltype = "Common Skill"
}
r.Components = append(r.Components, discord.NewSmallSeparator())
text := make([]string, 0, 3)
abils := make([]string, 0, 3)
for _, act := range s.Activations {
text, abils = text[:0], abils[:0]
if act.Precondition != "" {
text = append(text, "Precondition: "+formatCondition(act.Precondition))
}
text = append(text, "Condition: "+formatCondition(act.Condition))
var t string
switch {
case act.Duration < 0:
// passive; do nothing
case act.Duration == 0:
t = "Instantaneous "
case act.Duration >= 500e4:
t = "Permanent "
default:
t = "For " + act.Duration.String() + "s, "
}
for _, a := range act.Abilities {
abils = append(abils, a.String())
}
t += strings.Join(abils, ", ")
if act.Cooldown > 0 && act.Cooldown < 500e4 {
t += " on " + act.Cooldown.String() + "s cooldown"
}
text = append(text, t)
r.Components = append(r.Components, discord.NewTextDisplay(strings.Join(text, "\n")))
}
l := discord.NewTextDisplayf("%s ・ SP cost %d ・ Grade value %d ・ [Conditions on GameTora](https://gametora.com/umamusume/skill-condition-viewer?skill=%d)", skilltype, s.SPCost, s.GradeValue, s.ID)
r.Components = append(r.Components, discord.NewSmallSeparator(), l)
rel := make([]horse.Skill, 0, 4)
for _, id := range groups[s.Group] {
if id != 0 {
rel = append(rel, all[id])
}
}
if len(rel) > 1 {
buttons := make([]discord.InteractiveComponent, 0, 4)
for _, rs := range rel {
b := discord.NewSecondaryButton(rs.Name, fmt.Sprintf("/skill/%d", rs.ID))
if rs.ID == id {
b = b.AsDisabled()
}
buttons = append(buttons, b)
}
r.Components = append(r.Components, discord.NewActionRow(buttons...))
}
return r
}
func formatCondition(s string) string {
s = strings.ReplaceAll(s, "&", " & ")
if strings.ContainsRune(s, '@') {
return "```\n" + strings.ReplaceAll(s, "@", "\n@\n") + "```"
}
return "`" + s + "`"
}
func isDebuff(s horse.Skill) bool {
for _, act := range s.Activations {
for _, a := range act.Abilities {
if a.Value < 0 {
return true
}
}
}
return false
}
var skillGlobalAuto = sync.OnceValue(func() *autocomplete.Set[discord.AutocompleteChoice] {
var set autocomplete.Set[discord.AutocompleteChoice]
for _, id := range global.OrderedSkills {
s := global.AllSkills[id]
set.Add(s.Name, discord.AutocompleteChoiceString{Name: s.Name, Value: s.Name})
if s.UniqueOwner != "" {
if s.Rarity >= 3 {
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Unique: " + s.UniqueOwner, Value: s.Name})
} else {
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Inherited unique: " + s.UniqueOwner, Value: s.Name})
}
}
}
return &set
})

14
go.mod
View File

@@ -1,20 +1,30 @@
module git.sunturtle.xyz/zephyr/horse
go 1.24.1
go 1.25.5
require (
github.com/disgoorg/disgo v0.19.0-rc.15
github.com/junegunn/fzf v0.67.0
golang.org/x/sync v0.14.0
zombiezen.com/go/sqlite v1.4.2
)
require (
github.com/disgoorg/json/v2 v2.0.0 // indirect
github.com/disgoorg/omit v1.0.0 // indirect
github.com/disgoorg/snowflake/v2 v2.0.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.39.0 // indirect
modernc.org/libc v1.65.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect

36
go.sum
View File

@@ -1,15 +1,41 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disgoorg/disgo v0.19.0-rc.15 h1:x0NsV2gcbdjwuztsg2wYXw76p1Cpc8f6ByDrkPcfQtU=
github.com/disgoorg/disgo v0.19.0-rc.15/go.mod h1:14mgXzenkJqifkDmsEgU0zI1di6jNXodwX6L8geW33A=
github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI=
github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI=
github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78=
github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ=
github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro=
github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/junegunn/fzf v0.67.0 h1:naiOdIkV5/ZCfHgKQIV/f5YDWowl95G6yyOQqW8FeSo=
github.com/junegunn/fzf v0.67.0/go.mod h1:xlXX2/rmsccKQUnr9QOXPDi5DyV9cM0UjKy/huScBeE=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI=
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -17,12 +43,14 @@ golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=

View File

@@ -29,10 +29,10 @@ const (
RaceAsahiHaiFuturityStakes RaceID = 1022 // Asahi Hai Futurity Stakes
RaceArimaKinen RaceID = 1023 // Arima Kinen
RaceHopefulStakes RaceID = 1024 // Hopeful Stakes
RaceTakarazukaKinenAlternate RaceID = 1025 // Takarazuka Kinen
RaceKikukaShoAlternate RaceID = 1026 // Kikuka Sho
RaceTennoShoSpringAlternate RaceID = 1027 // Tenno Sho (Spring)
RaceSatsukiShoAlternate RaceID = 1028 // Satsuki Sho
RaceTakarazukaKinenAlt1025 RaceID = 1025 // Takarazuka Kinen
RaceKikukaShoAlt1026 RaceID = 1026 // Kikuka Sho
RaceTennoShoSpringAlt1027 RaceID = 1027 // Tenno Sho (Spring)
RaceSatsukiShoAlt1028 RaceID = 1028 // Satsuki Sho
RaceTeioSho RaceID = 1101 // Teio Sho
RaceJapanDirtDerby RaceID = 1102 // Japan Dirt Derby
RaceJBCLadiesClassic RaceID = 1103 // JBC Ladies Classic
@@ -73,7 +73,7 @@ const (
RaceDailyHaiJuniorStakes RaceID = 2032 // Daily Hai Junior Stakes
RaceStayersStakes RaceID = 2033 // Stayers Stakes
RaceHanshinCup RaceID = 2034 // Hanshin Cup
RaceSpringStakesAlternate RaceID = 2035 // Spring Stakes
RaceSpringStakesAlt2035 RaceID = 2035 // Spring Stakes
RaceKyotoKimpai RaceID = 3001 // Kyoto Kimpai
RaceNakayamaKimpai RaceID = 3002 // Nakayama Kimpai
RaceShinzanKinen RaceID = 3003 // Shinzan Kinen
@@ -413,27 +413,27 @@ var AllRaces = map[RaceID]Race{
Name: "Hopeful Stakes",
Thumbnail: 1024,
},
RaceTakarazukaKinenAlternate: {
RaceTakarazukaKinenAlt1025: {
ID: 1025,
Name: "Takarazuka Kinen" + " (Alternate)",
Name: "Takarazuka Kinen" + " (Alternate 1025)",
Thumbnail: 1012,
Primary: 1012,
},
RaceKikukaShoAlternate: {
RaceKikukaShoAlt1026: {
ID: 1026,
Name: "Kikuka Sho" + " (Alternate)",
Name: "Kikuka Sho" + " (Alternate 1026)",
Thumbnail: 1015,
Primary: 1015,
},
RaceTennoShoSpringAlternate: {
RaceTennoShoSpringAlt1027: {
ID: 1027,
Name: "Tenno Sho (Spring)" + " (Alternate)",
Name: "Tenno Sho (Spring)" + " (Alternate 1027)",
Thumbnail: 1027,
Primary: 1006,
},
RaceSatsukiShoAlternate: {
RaceSatsukiShoAlt1028: {
ID: 1028,
Name: "Satsuki Sho" + " (Alternate)",
Name: "Satsuki Sho" + " (Alternate 1028)",
Thumbnail: 1028,
Primary: 1005,
},
@@ -637,9 +637,9 @@ var AllRaces = map[RaceID]Race{
Name: "Hanshin Cup",
Thumbnail: 2034,
},
RaceSpringStakesAlternate: {
RaceSpringStakesAlt2035: {
ID: 2035,
Name: "Spring Stakes" + " (Alternate)",
Name: "Spring Stakes" + " (Alternate 2035)",
Thumbnail: 2010,
Primary: 2010,
},
@@ -1726,289 +1726,289 @@ var AllRaces = map[RaceID]Race{
}
var RaceNameToID = map[string]RaceID{
"February Stakes": 1001,
"Takamatsunomiya Kinen": 1002,
"Osaka Hai": 1003,
"Oka Sho": 1004,
"Satsuki Sho": 1005,
"Tenno Sho (Spring)": 1006,
"NHK Mile Cup": 1007,
"Victoria Mile": 1008,
"Japanese Oaks": 1009,
"Tokyo Yushun (Japanese Derby)": 1010,
"Yasuda Kinen": 1011,
"Takarazuka Kinen": 1012,
"Sprinters Stakes": 1013,
"Shuka Sho": 1014,
"Kikuka Sho": 1015,
"Tenno Sho (Autumn)": 1016,
"Queen Elizabeth II Cup": 1017,
"Mile Championship": 1018,
"Japan Cup": 1019,
"Champions Cup": 1020,
"Hanshin Juvenile Fillies": 1021,
"Asahi Hai Futurity Stakes": 1022,
"Arima Kinen": 1023,
"Hopeful Stakes": 1024,
"Takarazuka Kinen" + " (Alternate)": 1025,
"Kikuka Sho" + " (Alternate)": 1026,
"Tenno Sho (Spring)" + " (Alternate)": 1027,
"Satsuki Sho" + " (Alternate)": 1028,
"Teio Sho": 1101,
"Japan Dirt Derby": 1102,
"JBC Ladies Classic": 1103,
"JBC Sprint": 1104,
"JBC Classic": 1105,
"Tokyo Daishoten": 1106,
"Nikkei Shinshun Hai": 2001,
"Tokai Stakes": 2002,
"American JCC": 2003,
"Kyoto Kinen": 2004,
"Nakayama Kinen": 2005,
"Yayoi Sho": 2006,
"Kinko Sho": 2007,
"Fillies' Revue": 2008,
"Hanshin Daishoten": 2009,
"Spring Stakes": 2010,
"Nikkei Sho": 2011,
"Hanshin Umamusume Stakes": 2012,
"New Zealand Trophy": 2013,
"Milers Cup": 2014,
"Flora Stakes": 2015,
"Aoba Sho": 2016,
"Kyoto Shimbun Hai": 2017,
"Keio Hai Spring Cup": 2018,
"Meguro Kinen": 2019,
"Sapporo Kinen": 2020,
"Centaur Stakes": 2021,
"Rose Stakes": 2022,
"St. Lite Kinen": 2023,
"Kobe Shimbun Hai": 2024,
"All Comers": 2025,
"Mainichi Okan": 2026,
"Kyoto Daishoten": 2027,
"Fuchu Umamusume Stakes": 2028,
"Swan Stakes": 2029,
"Keio Hai Junior Stakes": 2030,
"Copa Republica Argentina": 2031,
"Daily Hai Junior Stakes": 2032,
"Stayers Stakes": 2033,
"Hanshin Cup": 2034,
"Spring Stakes" + " (Alternate)": 2035,
"Kyoto Kimpai": 3001,
"Nakayama Kimpai": 3002,
"Shinzan Kinen": 3003,
"Fairy Stakes": 3004,
"Aichi Hai": 3005,
"Keisei Hai": 3006,
"Silk Road Stakes": 3007,
"Negishi Stakes": 3008,
"Kisaragi Sho": 3009,
"Tokyo Shimbun Hai": 3010,
"Queen Cup": 3011,
"Kyodo News Hai": 3012,
"Kyoto Umamusume Stakes": 3013,
"Diamond Stakes": 3014,
"Kokura Daishoten": 3015,
"Arlington Cup": 3016,
"Hankyu Hai": 3017,
"Tulip Sho": 3018,
"Ocean Stakes": 3019,
"Nakayama Umamusume Stakes": 3020,
"Falcon Stakes": 3021,
"Flower Cup": 3022,
"Mainichi Hai": 3023,
"March Stakes": 3024,
"Lord Derby Challenge Trophy": 3025,
"Antares Stakes": 3026,
"Fukushima Umamusume Stakes": 3027,
"Niigata Daishoten": 3028,
"Heian Stakes": 3029,
"Naruo Kinen": 3030,
"Mermaid Stakes": 3031,
"Epsom Cup": 3032,
"Unicorn Stakes": 3033,
"Hakodate Sprint Stakes": 3034,
"CBC Sho": 3035,
"Radio Nikkei Sho": 3036,
"Procyon Stakes": 3037,
"Tanabata Sho": 3038,
"Hakodate Kinen": 3039,
"Chukyo Kinen": 3040,
"Hakodate Junior Stakes": 3041,
"Ibis Summer Dash": 3042,
"Queen Stakes": 3043,
"Kokura Kinen": 3044,
"Leopard Stakes": 3045,
"Sekiya Kinen": 3046,
"Elm Stakes": 3047,
"Kitakyushu Kinen": 3048,
"Niigata Junior Stakes": 3049,
"Keeneland Cup": 3050,
"Sapporo Junior Stakes": 3051,
"Kokura Junior Stakes": 3052,
"Niigata Kinen": 3053,
"Shion Stakes": 3054,
"Keisei Hai Autumn Handicap": 3055,
"Sirius Stakes": 3056,
"Saudi Arabia Royal Cup": 3057,
"Fuji Stakes": 3058,
"Artemis Stakes": 3059,
"Fantasy Stakes": 3060,
"Miyako Stakes": 3061,
"Musashino Stakes": 3062,
"Fukushima Kinen": 3063,
"Tokyo Sports Hai Junior Stakes": 3064,
"Kyoto Junior Stakes": 3065,
"Keihan Hai": 3066,
"Challenge Cup": 3067,
"Chunichi Shimbun Hai": 3068,
"Capella Stakes": 3069,
"Turquoise Stakes": 3070,
"Manyo Stakes": 4001,
"Junior Cup": 4002,
"Yodo Tankyori Stakes": 4003,
"Pollux Stakes": 4004,
"January Stakes": 4005,
"New Year Stakes": 4006,
"Kobai Stakes": 4007,
"Subaru Stakes": 4008,
"Wakagoma Stakes": 4009,
"Carbuncle Stakes": 4010,
"Shirafuji Stakes": 4011,
"Crocus Stakes": 4012,
"Yamato Stakes": 4013,
"Elfin Stakes": 4014,
"Rakuyo Stakes": 4015,
"Aldebaran Stakes": 4016,
"Valentine Stakes": 4017,
"Hyacinth Stakes": 4018,
"Sobu Stakes": 4019,
"Sumire Stakes": 4020,
"Osakajo Stakes": 4021,
"Polaris Stakes": 4022,
"Nigawa Stakes": 4023,
"Anemone Stakes": 4024,
"Shoryu Stakes": 4025,
"Kochi Stakes": 4026,
"Wakaba Stakes": 4027,
"Chiba Stakes": 4028,
"Rokko Stakes": 4030,
"Coral Stakes": 4031,
"Marguerite Stakes": 4032,
"Fukuryu Stakes": 4033,
"Wasurenagusa Sho": 4035,
"Keiyo Stakes": 4036,
"Shunrai Stakes": 4037,
"Fukushima Mimpo Hai": 4038,
"Tachibana Stakes": 4039,
"Oasis Stakes": 4040,
"Tennozan Stakes": 4041,
"Tango Stakes": 4042,
"Sweetpea Stakes": 4043,
"Tanigawadake Stakes": 4044,
"Principal Stakes": 4045,
"Metropolitan Stakes": 4046,
"Kurama Stakes": 4047,
"Brilliant Stakes": 4048,
"Miyakooji Stakes": 4049,
"Aoi Stakes": 4050,
"Ritto Stakes": 4051,
"Seiryu Stakes": 4052,
"May Stakes": 4053,
"Hosu Stakes": 4054,
"Idaten Stakes": 4055,
"Shirayuri Stakes": 4056,
"Keyaki Stakes": 4057,
"Azuchijo Stakes": 4058,
"Akhalteke Stakes": 4059,
"Tempozan Stakes": 4060,
"Yonago Stakes": 4061,
"Onuma Stakes": 4062,
"Paradise Stakes": 4063,
"Tomoe Sho": 4064,
"Marine Stakes": 4065,
"Meitetsu Hai": 4066,
"Chukyo Junior Stakes": 4068,
"Fukushima TV Open": 4069,
"Dahlia Sho": 4070,
"Sapporo Nikkei Open": 4071,
"UHB Sho": 4072,
"Aso Stakes": 4073,
"Phoenix Sho": 4074,
"Cosmos Sho": 4075,
"NST Sho": 4076,
"Clover Sho": 4077,
"Himawari Sho": 4078,
"BSN Sho": 4079,
"Kokura Nikkei Open": 4080,
"Toki Stakes": 4081,
"Tancho Stakes": 4082,
"Suzuran Sho": 4083,
"Enif Stakes": 4084,
"Nojigiku Stakes": 4085,
"Radio Nippon Sho": 4086,
"Kikyo Stakes": 4087,
"Fuyo Stakes": 4088,
"Canna Stakes": 4089,
"Port Island Stakes": 4090,
"Opal Stakes": 4091,
"Green Channel Cup": 4092,
"Momiji Stakes": 4093,
"October Stakes": 4094,
"Shinetsu Stakes": 4095,
"Ivy Stakes": 4096,
"Muromachi Stakes": 4097,
"Brazil Cup": 4098,
"Hagi Stakes": 4099,
"Cassiopeia Stakes": 4100,
"Lumiere Autumn Dash": 4101,
"Oro Cup": 4102,
"Fukushima Junior Stakes": 4103,
"Andromeda Stakes": 4104,
"Shimotsuki Stakes": 4105,
"Fukushima Minyu Cup": 4106,
"Capital Stakes": 4107,
"Autumn Leaf Stakes": 4108,
"Lapis Lazuli Stakes": 4109,
"Shiwasu Stakes": 4110,
"Rigel Stakes": 4111,
"Tanzanite Stakes": 4112,
"December Stakes": 4113,
"Christmas Rose Stakes": 4114,
"Galaxy Stakes": 4115,
"Betelgeuse Stakes": 4116,
"Kitakyushu Tankyori Stakes": 4118,
"Azumakofuji Stakes": 4119,
"Sleipnir Stakes": 4120,
"Sannomiya Stakes": 4121,
"Kanetsu Stakes": 4122,
"Nagatsuki Stakes": 4123,
"Uzumasa Stakes": 4124,
"Aster Sho": 4501,
"Saffron Sho": 4502,
"Rindo Sho": 4503,
"Shigiku Sho": 4504,
"Platanus Sho": 4505,
"Nadeshiko Sho": 4506,
"Hyakunichiso Tokubetsu": 4507,
"Kimmokusei Tokubetsu": 4508,
"Oxalis Sho": 4509,
"Kigiku Sho": 4510,
"Mochinoki Sho": 4511,
"Akamatsu Sho": 4512,
"Shumeigiku Sho": 4513,
"Cattleya Sho": 4514,
"Begonia Sho": 4515,
"Shiragiku Sho": 4516,
"Habotan Sho": 4517,
"Koyamaki Sho": 4518,
"Manryo Sho": 4519,
"Kuromatsu Sho": 4520,
"Erica Sho": 4521,
"Tsuwabuki Sho": 4522,
"Hiiragi Sho": 4523,
"Sazanka Sho": 4524,
"Kantsubaki Sho": 4525,
"Senryo Sho": 4526,
"February Stakes": 1001,
"Takamatsunomiya Kinen": 1002,
"Osaka Hai": 1003,
"Oka Sho": 1004,
"Satsuki Sho": 1005,
"Tenno Sho (Spring)": 1006,
"NHK Mile Cup": 1007,
"Victoria Mile": 1008,
"Japanese Oaks": 1009,
"Tokyo Yushun (Japanese Derby)": 1010,
"Yasuda Kinen": 1011,
"Takarazuka Kinen": 1012,
"Sprinters Stakes": 1013,
"Shuka Sho": 1014,
"Kikuka Sho": 1015,
"Tenno Sho (Autumn)": 1016,
"Queen Elizabeth II Cup": 1017,
"Mile Championship": 1018,
"Japan Cup": 1019,
"Champions Cup": 1020,
"Hanshin Juvenile Fillies": 1021,
"Asahi Hai Futurity Stakes": 1022,
"Arima Kinen": 1023,
"Hopeful Stakes": 1024,
"Takarazuka Kinen" + " (Alternate 1025)": 1025,
"Kikuka Sho" + " (Alternate 1026)": 1026,
"Tenno Sho (Spring)" + " (Alternate 1027)": 1027,
"Satsuki Sho" + " (Alternate 1028)": 1028,
"Teio Sho": 1101,
"Japan Dirt Derby": 1102,
"JBC Ladies Classic": 1103,
"JBC Sprint": 1104,
"JBC Classic": 1105,
"Tokyo Daishoten": 1106,
"Nikkei Shinshun Hai": 2001,
"Tokai Stakes": 2002,
"American JCC": 2003,
"Kyoto Kinen": 2004,
"Nakayama Kinen": 2005,
"Yayoi Sho": 2006,
"Kinko Sho": 2007,
"Fillies' Revue": 2008,
"Hanshin Daishoten": 2009,
"Spring Stakes": 2010,
"Nikkei Sho": 2011,
"Hanshin Umamusume Stakes": 2012,
"New Zealand Trophy": 2013,
"Milers Cup": 2014,
"Flora Stakes": 2015,
"Aoba Sho": 2016,
"Kyoto Shimbun Hai": 2017,
"Keio Hai Spring Cup": 2018,
"Meguro Kinen": 2019,
"Sapporo Kinen": 2020,
"Centaur Stakes": 2021,
"Rose Stakes": 2022,
"St. Lite Kinen": 2023,
"Kobe Shimbun Hai": 2024,
"All Comers": 2025,
"Mainichi Okan": 2026,
"Kyoto Daishoten": 2027,
"Fuchu Umamusume Stakes": 2028,
"Swan Stakes": 2029,
"Keio Hai Junior Stakes": 2030,
"Copa Republica Argentina": 2031,
"Daily Hai Junior Stakes": 2032,
"Stayers Stakes": 2033,
"Hanshin Cup": 2034,
"Spring Stakes" + " (Alternate 2035)": 2035,
"Kyoto Kimpai": 3001,
"Nakayama Kimpai": 3002,
"Shinzan Kinen": 3003,
"Fairy Stakes": 3004,
"Aichi Hai": 3005,
"Keisei Hai": 3006,
"Silk Road Stakes": 3007,
"Negishi Stakes": 3008,
"Kisaragi Sho": 3009,
"Tokyo Shimbun Hai": 3010,
"Queen Cup": 3011,
"Kyodo News Hai": 3012,
"Kyoto Umamusume Stakes": 3013,
"Diamond Stakes": 3014,
"Kokura Daishoten": 3015,
"Arlington Cup": 3016,
"Hankyu Hai": 3017,
"Tulip Sho": 3018,
"Ocean Stakes": 3019,
"Nakayama Umamusume Stakes": 3020,
"Falcon Stakes": 3021,
"Flower Cup": 3022,
"Mainichi Hai": 3023,
"March Stakes": 3024,
"Lord Derby Challenge Trophy": 3025,
"Antares Stakes": 3026,
"Fukushima Umamusume Stakes": 3027,
"Niigata Daishoten": 3028,
"Heian Stakes": 3029,
"Naruo Kinen": 3030,
"Mermaid Stakes": 3031,
"Epsom Cup": 3032,
"Unicorn Stakes": 3033,
"Hakodate Sprint Stakes": 3034,
"CBC Sho": 3035,
"Radio Nikkei Sho": 3036,
"Procyon Stakes": 3037,
"Tanabata Sho": 3038,
"Hakodate Kinen": 3039,
"Chukyo Kinen": 3040,
"Hakodate Junior Stakes": 3041,
"Ibis Summer Dash": 3042,
"Queen Stakes": 3043,
"Kokura Kinen": 3044,
"Leopard Stakes": 3045,
"Sekiya Kinen": 3046,
"Elm Stakes": 3047,
"Kitakyushu Kinen": 3048,
"Niigata Junior Stakes": 3049,
"Keeneland Cup": 3050,
"Sapporo Junior Stakes": 3051,
"Kokura Junior Stakes": 3052,
"Niigata Kinen": 3053,
"Shion Stakes": 3054,
"Keisei Hai Autumn Handicap": 3055,
"Sirius Stakes": 3056,
"Saudi Arabia Royal Cup": 3057,
"Fuji Stakes": 3058,
"Artemis Stakes": 3059,
"Fantasy Stakes": 3060,
"Miyako Stakes": 3061,
"Musashino Stakes": 3062,
"Fukushima Kinen": 3063,
"Tokyo Sports Hai Junior Stakes": 3064,
"Kyoto Junior Stakes": 3065,
"Keihan Hai": 3066,
"Challenge Cup": 3067,
"Chunichi Shimbun Hai": 3068,
"Capella Stakes": 3069,
"Turquoise Stakes": 3070,
"Manyo Stakes": 4001,
"Junior Cup": 4002,
"Yodo Tankyori Stakes": 4003,
"Pollux Stakes": 4004,
"January Stakes": 4005,
"New Year Stakes": 4006,
"Kobai Stakes": 4007,
"Subaru Stakes": 4008,
"Wakagoma Stakes": 4009,
"Carbuncle Stakes": 4010,
"Shirafuji Stakes": 4011,
"Crocus Stakes": 4012,
"Yamato Stakes": 4013,
"Elfin Stakes": 4014,
"Rakuyo Stakes": 4015,
"Aldebaran Stakes": 4016,
"Valentine Stakes": 4017,
"Hyacinth Stakes": 4018,
"Sobu Stakes": 4019,
"Sumire Stakes": 4020,
"Osakajo Stakes": 4021,
"Polaris Stakes": 4022,
"Nigawa Stakes": 4023,
"Anemone Stakes": 4024,
"Shoryu Stakes": 4025,
"Kochi Stakes": 4026,
"Wakaba Stakes": 4027,
"Chiba Stakes": 4028,
"Rokko Stakes": 4030,
"Coral Stakes": 4031,
"Marguerite Stakes": 4032,
"Fukuryu Stakes": 4033,
"Wasurenagusa Sho": 4035,
"Keiyo Stakes": 4036,
"Shunrai Stakes": 4037,
"Fukushima Mimpo Hai": 4038,
"Tachibana Stakes": 4039,
"Oasis Stakes": 4040,
"Tennozan Stakes": 4041,
"Tango Stakes": 4042,
"Sweetpea Stakes": 4043,
"Tanigawadake Stakes": 4044,
"Principal Stakes": 4045,
"Metropolitan Stakes": 4046,
"Kurama Stakes": 4047,
"Brilliant Stakes": 4048,
"Miyakooji Stakes": 4049,
"Aoi Stakes": 4050,
"Ritto Stakes": 4051,
"Seiryu Stakes": 4052,
"May Stakes": 4053,
"Hosu Stakes": 4054,
"Idaten Stakes": 4055,
"Shirayuri Stakes": 4056,
"Keyaki Stakes": 4057,
"Azuchijo Stakes": 4058,
"Akhalteke Stakes": 4059,
"Tempozan Stakes": 4060,
"Yonago Stakes": 4061,
"Onuma Stakes": 4062,
"Paradise Stakes": 4063,
"Tomoe Sho": 4064,
"Marine Stakes": 4065,
"Meitetsu Hai": 4066,
"Chukyo Junior Stakes": 4068,
"Fukushima TV Open": 4069,
"Dahlia Sho": 4070,
"Sapporo Nikkei Open": 4071,
"UHB Sho": 4072,
"Aso Stakes": 4073,
"Phoenix Sho": 4074,
"Cosmos Sho": 4075,
"NST Sho": 4076,
"Clover Sho": 4077,
"Himawari Sho": 4078,
"BSN Sho": 4079,
"Kokura Nikkei Open": 4080,
"Toki Stakes": 4081,
"Tancho Stakes": 4082,
"Suzuran Sho": 4083,
"Enif Stakes": 4084,
"Nojigiku Stakes": 4085,
"Radio Nippon Sho": 4086,
"Kikyo Stakes": 4087,
"Fuyo Stakes": 4088,
"Canna Stakes": 4089,
"Port Island Stakes": 4090,
"Opal Stakes": 4091,
"Green Channel Cup": 4092,
"Momiji Stakes": 4093,
"October Stakes": 4094,
"Shinetsu Stakes": 4095,
"Ivy Stakes": 4096,
"Muromachi Stakes": 4097,
"Brazil Cup": 4098,
"Hagi Stakes": 4099,
"Cassiopeia Stakes": 4100,
"Lumiere Autumn Dash": 4101,
"Oro Cup": 4102,
"Fukushima Junior Stakes": 4103,
"Andromeda Stakes": 4104,
"Shimotsuki Stakes": 4105,
"Fukushima Minyu Cup": 4106,
"Capital Stakes": 4107,
"Autumn Leaf Stakes": 4108,
"Lapis Lazuli Stakes": 4109,
"Shiwasu Stakes": 4110,
"Rigel Stakes": 4111,
"Tanzanite Stakes": 4112,
"December Stakes": 4113,
"Christmas Rose Stakes": 4114,
"Galaxy Stakes": 4115,
"Betelgeuse Stakes": 4116,
"Kitakyushu Tankyori Stakes": 4118,
"Azumakofuji Stakes": 4119,
"Sleipnir Stakes": 4120,
"Sannomiya Stakes": 4121,
"Kanetsu Stakes": 4122,
"Nagatsuki Stakes": 4123,
"Uzumasa Stakes": 4124,
"Aster Sho": 4501,
"Saffron Sho": 4502,
"Rindo Sho": 4503,
"Shigiku Sho": 4504,
"Platanus Sho": 4505,
"Nadeshiko Sho": 4506,
"Hyakunichiso Tokubetsu": 4507,
"Kimmokusei Tokubetsu": 4508,
"Oxalis Sho": 4509,
"Kigiku Sho": 4510,
"Mochinoki Sho": 4511,
"Akamatsu Sho": 4512,
"Shumeigiku Sho": 4513,
"Cattleya Sho": 4514,
"Begonia Sho": 4515,
"Shiragiku Sho": 4516,
"Habotan Sho": 4517,
"Koyamaki Sho": 4518,
"Manryo Sho": 4519,
"Kuromatsu Sho": 4520,
"Erica Sho": 4521,
"Tsuwabuki Sho": 4522,
"Hiiragi Sho": 4523,
"Sazanka Sho": 4524,
"Kantsubaki Sho": 4525,
"Senryo Sho": 4526,
}

View File

@@ -32,10 +32,10 @@ pub type race
Asahi-Hai-Futurity-Stakes
Arima-Kinen
Hopeful-Stakes
Takarazuka-Kinen-Alternate
Kikuka-Sho-Alternate
Tenno-Sho-Spring-Alternate
Satsuki-Sho-Alternate
Takarazuka-Kinen-Alt1025
Kikuka-Sho-Alt1026
Tenno-Sho-Spring-Alt1027
Satsuki-Sho-Alt1028
Teio-Sho
Japan-Dirt-Derby
JBC-Ladies-Classic
@@ -76,7 +76,7 @@ pub type race
Daily-Hai-Junior-Stakes
Stayers-Stakes
Hanshin-Cup
Spring-Stakes-Alternate
Spring-Stakes-Alt2035
Kyoto-Kimpai
Nakayama-Kimpai
Shinzan-Kinen
@@ -321,10 +321,10 @@ pub fun race-id(r: race): race-id
Asahi-Hai-Futurity-Stakes -> Race-id(1022)
Arima-Kinen -> Race-id(1023)
Hopeful-Stakes -> Race-id(1024)
Takarazuka-Kinen-Alternate -> Race-id(1025)
Kikuka-Sho-Alternate -> Race-id(1026)
Tenno-Sho-Spring-Alternate -> Race-id(1027)
Satsuki-Sho-Alternate -> Race-id(1028)
Takarazuka-Kinen-Alt1025 -> Race-id(1025)
Kikuka-Sho-Alt1026 -> Race-id(1026)
Tenno-Sho-Spring-Alt1027 -> Race-id(1027)
Satsuki-Sho-Alt1028 -> Race-id(1028)
Teio-Sho -> Race-id(1101)
Japan-Dirt-Derby -> Race-id(1102)
JBC-Ladies-Classic -> Race-id(1103)
@@ -365,7 +365,7 @@ pub fun race-id(r: race): race-id
Daily-Hai-Junior-Stakes -> Race-id(2032)
Stayers-Stakes -> Race-id(2033)
Hanshin-Cup -> Race-id(2034)
Spring-Stakes-Alternate -> Race-id(2035)
Spring-Stakes-Alt2035 -> Race-id(2035)
Kyoto-Kimpai -> Race-id(3001)
Nakayama-Kimpai -> Race-id(3002)
Shinzan-Kinen -> Race-id(3003)
@@ -609,10 +609,10 @@ pub val all = [
Asahi-Hai-Futurity-Stakes,
Arima-Kinen,
Hopeful-Stakes,
Takarazuka-Kinen-Alternate,
Kikuka-Sho-Alternate,
Tenno-Sho-Spring-Alternate,
Satsuki-Sho-Alternate,
Takarazuka-Kinen-Alt1025,
Kikuka-Sho-Alt1026,
Tenno-Sho-Spring-Alt1027,
Satsuki-Sho-Alt1028,
Teio-Sho,
Japan-Dirt-Derby,
JBC-Ladies-Classic,
@@ -653,7 +653,7 @@ pub val all = [
Daily-Hai-Junior-Stakes,
Stayers-Stakes,
Hanshin-Cup,
Spring-Stakes-Alternate,
Spring-Stakes-Alt2035,
Kyoto-Kimpai,
Nakayama-Kimpai,
Shinzan-Kinen,
@@ -897,10 +897,10 @@ val name2id: rbmap<string, race-id> = rb-map/empty()
.set("Asahi Hai Futurity Stakes", Race-id(1022))
.set("Arima Kinen", Race-id(1023))
.set("Hopeful Stakes", Race-id(1024))
.set("Takarazuka Kinen" ++ " (Alternate)", Race-id(1025))
.set("Kikuka Sho" ++ " (Alternate)", Race-id(1026))
.set("Tenno Sho (Spring)" ++ " (Alternate)", Race-id(1027))
.set("Satsuki Sho" ++ " (Alternate)", Race-id(1028))
.set("Takarazuka Kinen" ++ " (Alternate 1025)", Race-id(1025))
.set("Kikuka Sho" ++ " (Alternate 1026)", Race-id(1026))
.set("Tenno Sho (Spring)" ++ " (Alternate 1027)", Race-id(1027))
.set("Satsuki Sho" ++ " (Alternate 1028)", Race-id(1028))
.set("Teio Sho", Race-id(1101))
.set("Japan Dirt Derby", Race-id(1102))
.set("JBC Ladies Classic", Race-id(1103))
@@ -941,7 +941,7 @@ val name2id: rbmap<string, race-id> = rb-map/empty()
.set("Daily Hai Junior Stakes", Race-id(2032))
.set("Stayers Stakes", Race-id(2033))
.set("Hanshin Cup", Race-id(2034))
.set("Spring Stakes" ++ " (Alternate)", Race-id(2035))
.set("Spring Stakes" ++ " (Alternate 2035)", Race-id(2035))
.set("Kyoto Kimpai", Race-id(3001))
.set("Nakayama Kimpai", Race-id(3002))
.set("Shinzan Kinen", Race-id(3003))
@@ -1160,13 +1160,13 @@ val name2id: rbmap<string, race-id> = rb-map/empty()
.set("Senryo Sho", Race-id(4526))
// Get the race ID that has the given exact name.
// Alternate versions of races have " (Alternate)" in their names.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the name, the result is an invalid ID.
pub fun from-name(name: string): race-id
name2id.lookup(name).default(Race-id(0))
// Get the name for a race.
// Alternate versions of races have " (Alternate)" in their names.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the ID, the result is the numeric ID.
pub fun show(r: race-id): string
match r.game-id
@@ -1194,10 +1194,10 @@ pub fun show(r: race-id): string
1022 -> "Asahi Hai Futurity Stakes"
1023 -> "Arima Kinen"
1024 -> "Hopeful Stakes"
1025 -> "Takarazuka Kinen" ++ " (Alternate)"
1026 -> "Kikuka Sho" ++ " (Alternate)"
1027 -> "Tenno Sho (Spring)" ++ " (Alternate)"
1028 -> "Satsuki Sho" ++ " (Alternate)"
1025 -> "Takarazuka Kinen" ++ " (Alternate 1025)"
1026 -> "Kikuka Sho" ++ " (Alternate 1026)"
1027 -> "Tenno Sho (Spring)" ++ " (Alternate 1027)"
1028 -> "Satsuki Sho" ++ " (Alternate 1028)"
1101 -> "Teio Sho"
1102 -> "Japan Dirt Derby"
1103 -> "JBC Ladies Classic"
@@ -1238,7 +1238,7 @@ pub fun show(r: race-id): string
2032 -> "Daily Hai Junior Stakes"
2033 -> "Stayers Stakes"
2034 -> "Hanshin Cup"
2035 -> "Spring Stakes" ++ " (Alternate)"
2035 -> "Spring Stakes" ++ " (Alternate 2035)"
3001 -> "Kyoto Kimpai"
3002 -> "Nakayama Kimpai"
3003 -> "Shinzan Kinen"

View File

@@ -151,18 +151,18 @@ pub type saddle
Chunichi-Shimbun-Hai
Capella-S
Turquoise-S
Classic-Triple-Crown-Alt1
Senior-Spring-Triple-Crown-Alt1
Dual-Grand-Prix-Alt1
Takarazuka-Kinen-Alt1
Kikuka-Sho-Alt1
Spring-S-Alt1
Classic-Triple-Crown-Alt144
Senior-Spring-Triple-Crown-Alt145
Dual-Grand-Prix-Alt146
Takarazuka-Kinen-Alt147
Kikuka-Sho-Alt148
Spring-S-Alt149
Aoi-S
Senior-Spring-Triple-Crown-Alt2
Tenno-Sweep-Alt1
Tenno-Sho-Spring-Alt1
Classic-Triple-Crown-Alt2
Satsuki-Sho-Alt1
Senior-Spring-Triple-Crown-Alt151
Tenno-Sweep-Alt152
Tenno-Sho-Spring-Alt153
Classic-Triple-Crown-Alt154
Satsuki-Sho-Alt155
// Get the saddle ID for a saddle.
pub fun saddle-id(s: saddle): saddle-id
@@ -310,18 +310,18 @@ pub fun saddle-id(s: saddle): saddle-id
Chunichi-Shimbun-Hai -> Saddle-id(141)
Capella-S -> Saddle-id(142)
Turquoise-S -> Saddle-id(143)
Classic-Triple-Crown-Alt1 -> Saddle-id(144)
Senior-Spring-Triple-Crown-Alt1 -> Saddle-id(145)
Dual-Grand-Prix-Alt1 -> Saddle-id(146)
Takarazuka-Kinen-Alt1 -> Saddle-id(147)
Kikuka-Sho-Alt1 -> Saddle-id(148)
Spring-S-Alt1 -> Saddle-id(149)
Classic-Triple-Crown-Alt144 -> Saddle-id(144)
Senior-Spring-Triple-Crown-Alt145 -> Saddle-id(145)
Dual-Grand-Prix-Alt146 -> Saddle-id(146)
Takarazuka-Kinen-Alt147 -> Saddle-id(147)
Kikuka-Sho-Alt148 -> Saddle-id(148)
Spring-S-Alt149 -> Saddle-id(149)
Aoi-S -> Saddle-id(150)
Senior-Spring-Triple-Crown-Alt2 -> Saddle-id(151)
Tenno-Sweep-Alt1 -> Saddle-id(152)
Tenno-Sho-Spring-Alt1 -> Saddle-id(153)
Classic-Triple-Crown-Alt2 -> Saddle-id(154)
Satsuki-Sho-Alt1 -> Saddle-id(155)
Senior-Spring-Triple-Crown-Alt151 -> Saddle-id(151)
Tenno-Sweep-Alt152 -> Saddle-id(152)
Tenno-Sho-Spring-Alt153 -> Saddle-id(153)
Classic-Triple-Crown-Alt154 -> Saddle-id(154)
Satsuki-Sho-Alt155 -> Saddle-id(155)
// List of all saddles in ID order for easy iterating.
pub val all = [
@@ -468,22 +468,22 @@ pub val all = [
Chunichi-Shimbun-Hai,
Capella-S,
Turquoise-S,
Classic-Triple-Crown-Alt1,
Senior-Spring-Triple-Crown-Alt1,
Dual-Grand-Prix-Alt1,
Takarazuka-Kinen-Alt1,
Kikuka-Sho-Alt1,
Spring-S-Alt1,
Classic-Triple-Crown-Alt144,
Senior-Spring-Triple-Crown-Alt145,
Dual-Grand-Prix-Alt146,
Takarazuka-Kinen-Alt147,
Kikuka-Sho-Alt148,
Spring-S-Alt149,
Aoi-S,
Senior-Spring-Triple-Crown-Alt2,
Tenno-Sweep-Alt1,
Tenno-Sho-Spring-Alt1,
Classic-Triple-Crown-Alt2,
Satsuki-Sho-Alt1,
Senior-Spring-Triple-Crown-Alt151,
Tenno-Sweep-Alt152,
Tenno-Sho-Spring-Alt153,
Classic-Triple-Crown-Alt154,
Satsuki-Sho-Alt155,
]
// Get the name for a saddle.
// Alternate versions of saddles have an indication of such in their names.
// Alternate versions of saddles have an indication of their ID in their names.
// If no saddle matches the ID, the result contains the numeric ID.
pub fun show(s: saddle-id): string
match s.game-id
@@ -630,18 +630,18 @@ pub fun show(s: saddle-id): string
141 -> "Chunichi Shimbun Hai"
142 -> "Capella S."
143 -> "Turquoise S."
144 -> "Classic Triple Crown" ++ " (Alternate 1)"
145 -> "Senior Spring Triple Crown" ++ " (Alternate 1)"
146 -> "Dual Grand Prix" ++ " (Alternate 1)"
147 -> "Takarazuka Kinen" ++ " (Alternate 1)"
148 -> "Kikuka Sho" ++ " (Alternate 1)"
149 -> "Spring S." ++ " (Alternate 1)"
144 -> "Classic Triple Crown" ++ " (Alternate 144)"
145 -> "Senior Spring Triple Crown" ++ " (Alternate 145)"
146 -> "Dual Grand Prix" ++ " (Alternate 146)"
147 -> "Takarazuka Kinen" ++ " (Alternate 147)"
148 -> "Kikuka Sho" ++ " (Alternate 148)"
149 -> "Spring S." ++ " (Alternate 149)"
150 -> "Aoi S."
151 -> "Senior Spring Triple Crown" ++ " (Alternate 2)"
152 -> "Tenno Sweep" ++ " (Alternate 1)"
153 -> "Tenno Sho (Spring)" ++ " (Alternate 1)"
154 -> "Classic Triple Crown" ++ " (Alternate 2)"
155 -> "Satsuki Sho" ++ " (Alternate 1)"
151 -> "Senior Spring Triple Crown" ++ " (Alternate 151)"
152 -> "Tenno Sweep" ++ " (Alternate 152)"
153 -> "Tenno Sho (Spring)" ++ " (Alternate 153)"
154 -> "Classic Triple Crown" ++ " (Alternate 154)"
155 -> "Satsuki Sho" ++ " (Alternate 155)"
x -> "saddle " ++ x.show
// Get the list of races that entitle a horse to a saddle.

View File

@@ -328,6 +328,7 @@ type Race struct {
Grade int
ThumbnailID int
Primary int
Alternate int
}
func Races(ctx context.Context, db *sqlitex.Pool) ([]Race, error) {
@@ -357,6 +358,7 @@ func Races(ctx context.Context, db *sqlitex.Pool) ([]Race, error) {
Grade: stmt.ColumnInt(2),
ThumbnailID: stmt.ColumnInt(3),
Primary: stmt.ColumnInt(4),
Alternate: stmt.ColumnInt(5),
}
r = append(r, race)
}

View File

@@ -7,15 +7,15 @@ import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $r := $.Races }}
Race{{ goenum $r.Name }}{{ if ne $r.Primary $r.ID }}Alternate{{ end }} RaceID = {{ $r.ID }} // {{ $r.Name }}
Race{{ goenum $r.Name }}{{ if $r.Alternate }}Alt{{ $r.ID }}{{ end }} RaceID = {{ $r.ID }} // {{ $r.Name }}
{{- end }}
)
var AllRaces = map[RaceID]Race{
{{- range $r := $.Races }}
Race{{ goenum $r.Name }}{{ if ne $r.Primary $r.ID }}Alternate{{ end }}: {
Race{{ goenum $r.Name }}{{ if $r.Alternate }}Alt{{ $r.ID }}{{ end }}: {
ID: {{ $r.ID }},
Name: {{ printf "%q" $r.Name }}{{ if ne $r.Primary $r.ID }} + " (Alternate)"{{ end }},
Name: {{ printf "%q" $r.Name }}{{ if $r.Alternate }} + " (Alternate {{ $r.ID }})"{{ end }},
Thumbnail: {{ $r.ThumbnailID }},
{{- if ne $r.Primary $r.ID }}
Primary: {{ $r.Primary }},
@@ -26,7 +26,7 @@ var AllRaces = map[RaceID]Race{
var RaceNameToID = map[string]RaceID{
{{- range $r := $.Races }}
{{ printf "%q" $r.Name }}{{ if ne $r.Primary $r.ID }} + " (Alternate)"{{ end }}: {{ $r.ID }},
{{ printf "%q" $r.Name }}{{ if $r.Alternate }} + " (Alternate {{ $r.ID }})"{{ end }}: {{ $r.ID }},
{{- end }}
}
{{ end }}

View File

@@ -10,41 +10,41 @@ pub import horse/race
// Enumeration of all races for type-safe programming.
pub type race
{{- range $r := $.Races }}
{{ kkenum $r.Name }}{{ if ne $r.Primary $r.ID }}-Alternate{{ end }}
{{ kkenum $r.Name }}{{ if $r.Alternate }}-Alt{{ $r.ID }}{{ end }}
{{- end }}
// Get the race ID for a race.
pub fun race-id(r: race): race-id
match r
{{- range $r := $.Races }}
{{ kkenum $r.Name }}{{ if ne $r.Primary $r.ID }}-Alternate{{ end }} -> Race-id({{ $r.ID }})
{{ kkenum $r.Name }}{{ if $r.Alternate }}-Alt{{ $r.ID }}{{ end }} -> Race-id({{ $r.ID }})
{{- end }}
// List of all races in ID order for easy iterating.
pub val all = [
{{- range $r := $.Races }}
{{ kkenum $r.Name }}{{ if ne $r.Primary $r.ID }}-Alternate{{ end }},
{{ kkenum $r.Name }}{{ if $r.Alternate }}-Alt{{ $r.ID }}{{ end }},
{{- end }}
]
val name2id: rbmap<string, race-id> = rb-map/empty()
{{- range $r := $.Races }}
.set({{ printf "%q" $r.Name }}{{ if ne $r.Primary $r.ID }} ++ " (Alternate)"{{ end }}, Race-id({{ $r.ID }}))
.set({{ printf "%q" $r.Name }}{{ if $r.Alternate }} ++ " (Alternate {{ $r.ID }})"{{ end }}, Race-id({{ $r.ID }}))
{{- end }}
// Get the race ID that has the given exact name.
// Alternate versions of races have " (Alternate)" in their names.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the name, the result is an invalid ID.
pub fun from-name(name: string): race-id
name2id.lookup(name).default(Race-id(0))
// Get the name for a race.
// Alternate versions of races have " (Alternate)" in their names.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the ID, the result is the numeric ID.
pub fun show(r: race-id): string
match r.game-id
{{- range $r := $.Races }}
{{ $r.ID }} -> {{ printf "%q" $r.Name }}{{ if ne $r.Primary $r.ID }} ++ " (Alternate)"{{ end }}
{{ $r.ID }} -> {{ printf "%q" $r.Name }}{{ if $r.Alternate }} ++ " (Alternate {{ $r.ID }})"{{ end }}
{{- end }}
x -> "race " ++ x.show
@@ -72,7 +72,7 @@ pub fun thumbnail(r: race-id): race-thumbnail-id
pub fun primary(r: race-id): race-id
match r.game-id
{{- range $r := $.Races }}
{{- if ne $r.ID $r.Primary }}
{{- if $r.Alternate }}
{{ $r.ID }} -> Race-id({{ $r.Primary }})
{{- end }}
{{- end }}

View File

@@ -6,7 +6,8 @@ SELECT
race_names.name,
race.grade,
race.thumbnail_id,
MIN(race.id) OVER (PARTITION BY race_names.name) AS "primary"
MIN(race.id) OVER (PARTITION BY race_names.name) AS "primary",
ROW_NUMBER() OVER (PARTITION BY race_names.name ORDER BY race.id) - 1 AS "alternate"
FROM race
JOIN race_names ON race.id = race_names.id
WHERE race."group" = 1

View File

@@ -10,30 +10,30 @@ pub import horse/{{ $.Region }}/race
// Enumeration of all saddles for type-safe programming.
pub type saddle
{{- range $s := $.Saddles }}
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.Alternate }}{{ end }}
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.ID }}{{ end }}
{{- end }}
// Get the saddle ID for a saddle.
pub fun saddle-id(s: saddle): saddle-id
match s
{{- range $s := $.Saddles }}
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.Alternate }}{{ end }} -> Saddle-id({{ $s.ID }})
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.ID }}{{ end }} -> Saddle-id({{ $s.ID }})
{{- end }}
// List of all saddles in ID order for easy iterating.
pub val all = [
{{- range $s := $.Saddles }}
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.Alternate }}{{ end }},
{{ kkenum $s.Name }}{{ if $s.Alternate }}-Alt{{ $s.ID }}{{ end }},
{{- end }}
]
// Get the name for a saddle.
// Alternate versions of saddles have an indication of such in their names.
// Alternate versions of saddles have an indication of their ID in their names.
// If no saddle matches the ID, the result contains the numeric ID.
pub fun show(s: saddle-id): string
match s.game-id
{{- range $s := $.Saddles }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}{{ if $s.Alternate }} ++ " (Alternate {{ $s.Alternate }})"{{ end }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}{{ if $s.Alternate }} ++ " (Alternate {{ $s.ID }})"{{ end }}
{{- end }}
x -> "saddle " ++ x.show