horse, mdb, cmd/horsebot: racecourses
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user