Files
horse/mdb/racecourse.go
zephyr 3bf17944b3
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
horse, mdb, cmd/horsebot: racecourses
2026-09-08 15:56:01 -04:00

172 lines
4.7 KiB
Go

package mdb
import (
"cmp"
"context"
"embed"
"encoding/json"
"fmt"
"slices"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
//go:embed sql/racecourse.sql
var racecourseSQL string
func Racecourses(ctx context.Context, db *sqlitex.Pool) ([]horse.Racecourse, error) {
return load(ctx, db, nil, racecourseSQL, func(s *sqlite.Stmt) horse.Racecourse {
c := horse.Racecourse{
ID: horse.RacecourseID(s.ColumnInt(0)),
TrackID: s.ColumnInt32(1),
TrackName: s.ColumnText(2),
Distance: float32(s.ColumnFloat(3)),
Ground: int8(s.ColumnInt(4)),
InOut: int8(s.ColumnInt(5)),
Turn: int8(s.ColumnInt(6)),
LaneMax: horse.TenThousandths(s.ColumnInt32(7)),
Threshold1: int8(s.ColumnInt(8)),
Threshold2: int8(s.ColumnInt(9)),
LaneStart: laneStarts[s.ColumnInt(10)-1],
}
ls := s.ColumnInt(10) - 1
if 0 <= ls && ls < len(laneStarts) {
c.LaneStart = laneStarts[ls]
}
evts := courseEvents(c.ID)
c.Spans, c.Slopes, c.MoveLanePoint = spansslopes(c.ID, evts, c.Distance)
return c
})
}
var laneStarts = [4][18]horse.TenThousandths{
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5333, 5888, 6444, 7000, 7555, 8111, 8666, 9222, 9777},
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5000, 5555, 6111, 6666, 7222, 7777, 8333, 8888, 9444},
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5000, 5555, 6111, 6666, 7222, 8811, 9366, 9922, 10477},
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4777, 5333, 5888, 6444, 7000, 7555, 8111, 8666, 9222, 9777},
}
//go:embed coursedata/*.json
var coursedataJSON embed.FS
type courseParams struct {
CourseParams []courseEvent `json:"courseParams"`
}
type courseEvent struct {
Type int8 `json:"_paramType"`
Values [2]int32 `json:"_values"`
Distance float32 `json:"_distance"`
}
func cmpEvents(a, b courseEvent) int {
if a.Distance == b.Distance {
return -cmp.Compare(a.Type, b.Type) // ensure straights are before corners
}
return cmp.Compare(a.Distance, b.Distance)
}
func courseEvents(id horse.RacecourseID) []courseEvent {
f, err := coursedataJSON.Open(fmt.Sprintf("coursedata/%d.json", id))
if err != nil {
return nil
}
d := json.NewDecoder(f)
d.DisallowUnknownFields()
var p courseParams
if err := d.Decode(&p); err != nil {
return nil
}
slices.SortFunc(p.CourseParams, cmpEvents)
return p.CourseParams
}
func spansslopes(id horse.RacecourseID, evts []courseEvent, length float32) ([]horse.RacecourseSpan, []horse.Slope, float32) {
// ensure spans and slopes are always non-nil for json
spans := []horse.RacecourseSpan{}
slopes := []horse.Slope{}
var ml float32
for _, evt := range evts {
var span horse.RacecourseSpan
switch evt.Type {
default:
panic(fmt.Errorf("horse: racecourse %d: unknown course event type %+v", id, evt))
case 1, 4, 5, 6, 8, 9, 10:
// Other events, e.g. jikkyo and bgm triggers.
// We aren't interested.
continue
case 3:
// Lane max change.
// We don't use this yet, but we might one day.
continue
case 7:
// Move lane point.
ml = evt.Distance
continue
case 11:
// Slope.
slope := horse.Slope{
Per: horse.TenThousandths(evt.Values[0]),
Start: evt.Distance,
End: evt.Distance + float32(evt.Values[1]),
}
slopes = append(slopes, slope)
continue
case 0:
// Corner.
span = horse.RacecourseSpan{
Start: evt.Distance,
End: evt.Distance + float32(evt.Values[1]),
Corner: int8(evt.Values[0]),
}
if ml == 0 {
ml = span.Start
}
// Continue out of the switch.
case 2:
// Straight.
if evt.Values[0] == 2 {
// End of the straight started by the previous event.
spans[len(spans)-1].End = evt.Distance
continue
}
// Start of the straight.
span = horse.RacecourseSpan{
Start: evt.Distance,
Straight: true,
FarStraight: evt.Values[1] == 2,
}
// Continue out of the switch.
}
// At this point, we are defining either a corner or a new straight.
var prev horse.RacecourseSpan
if len(spans) != 0 {
prev = spans[len(spans)-1]
}
if span.Start != prev.End {
// Longchamp 1000 (which doesn't appear to actually be used but w/e)
// defines straights by their start positions only, so check for the
// previous event being a straight with no end.
if prev.Straight && prev.End == 0 {
spans[len(spans)-1].End = span.Start
} else {
// Otherwise, insert a new span which is neither straight nor corner.
spans = append(spans, horse.RacecourseSpan{Start: prev.End, End: span.Start})
}
}
spans = append(spans, span)
}
// Always set the last span to end where the race does.
if len(spans) > 0 {
spans[len(spans)-1].End = length
}
return spans, slopes, ml
}