zenno/race/skillzone: new tool for viewing skill activation locations
Fixes #48.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import * as Plot from '@observablehq/plot';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import * as race from '$lib/race';
|
||||
import * as racecourse from '$lib/data/racecourse';
|
||||
|
||||
export interface DrawSpan {
|
||||
fy: string;
|
||||
x1: number;
|
||||
x2: number;
|
||||
color: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
course: racecourse.Course;
|
||||
title?: string;
|
||||
class?: ClassValue | null;
|
||||
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
|
||||
fontColor?: string;
|
||||
draw: DrawSpan[];
|
||||
}
|
||||
|
||||
let { course, title, class: className, plotOptions, fontColor, draw }: Props = $props();
|
||||
|
||||
let width = $state(0);
|
||||
let height = $state(0);
|
||||
|
||||
const phases = $derived(race.phases(course.distance));
|
||||
const phaseX = $derived([
|
||||
0,
|
||||
phases[race.Phase.EarlyRace],
|
||||
phases[race.Phase.MidRace],
|
||||
phases[race.Phase.LateRace],
|
||||
phases[race.Phase.LastSpurt],
|
||||
]);
|
||||
const sectionX = $derived([0, ...racecourse.SECTIONS.map((s) => course.distance * s.endRate)]);
|
||||
|
||||
const drawTitles = $derived(draw.map((d) => d.fy));
|
||||
|
||||
const makeChart: Attachment = (el) => {
|
||||
$effect(() => {
|
||||
el?.firstChild?.remove();
|
||||
el?.append(
|
||||
Plot.plot({
|
||||
width: width,
|
||||
height,
|
||||
clip: true,
|
||||
title,
|
||||
marginLeft: 50,
|
||||
x: {
|
||||
domain: [0, course.distance],
|
||||
ticks: phaseX,
|
||||
},
|
||||
fy: {
|
||||
label: null,
|
||||
padding: 0,
|
||||
domain: drawTitles,
|
||||
},
|
||||
...plotOptions,
|
||||
marks: [
|
||||
Plot.barX(draw, {
|
||||
x1: 'x1',
|
||||
x2: 'x2',
|
||||
fy: 'fy',
|
||||
fill: 'color',
|
||||
fillOpacity: 0.8,
|
||||
title: 'label',
|
||||
}),
|
||||
Plot.text(draw, { x: 'x1', fy: 'fy', text: 'label', dx: 2, textAnchor: 'start', fontSize: 16, fill: 'black' }),
|
||||
Plot.ruleX(sectionX, { opacity: 0.25 }),
|
||||
Plot.ruleX([phases[0], phases[1]], { opacity: 0.5 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
|
||||
<div role="img" {@attach makeChart}>
|
||||
<span>the chart seems to have didn't</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as z from 'zod';
|
||||
import * as race from '$lib/race';
|
||||
|
||||
export enum InOut {
|
||||
None = 1,
|
||||
Inner = 2,
|
||||
Outer = 3,
|
||||
OuterToInner = 4,
|
||||
}
|
||||
|
||||
export enum Turn {
|
||||
Right = 1,
|
||||
Left = 2,
|
||||
RightStretch = 3,
|
||||
LeftStretch = 4,
|
||||
}
|
||||
|
||||
const Span = z.strictObject({
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
straight: z.literal([1, 2]).optional(),
|
||||
corner: z.literal([1, 2, 3, 4]).optional(),
|
||||
});
|
||||
|
||||
export type Span = z.infer<typeof Span>;
|
||||
|
||||
const Slope = z.strictObject({
|
||||
per: z.int32(),
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
});
|
||||
|
||||
export type Slope = z.infer<typeof Slope>;
|
||||
|
||||
export const Course = z.strictObject({
|
||||
race_course_id: z.int32(),
|
||||
race_track_id: z.int32(),
|
||||
race_track_name: z.string(),
|
||||
distance: z.int32(),
|
||||
ground: z.enum(race.Surface),
|
||||
inout: z.enum(InOut),
|
||||
turn: z.enum(Turn),
|
||||
lane_max: z.int32(),
|
||||
threshold1: z.enum(race.Stat).optional(),
|
||||
threshold2: z.enum(race.Stat).optional(),
|
||||
lane_start: z.array(z.int32()).length(18),
|
||||
spans: z.array(Span),
|
||||
slopes: z.array(Slope),
|
||||
move_lane_point: z.number(),
|
||||
});
|
||||
|
||||
export type Course = z.infer<typeof Course>;
|
||||
|
||||
export async function racecourses(): Promise<Course[]> {
|
||||
const resp = await fetch('/api/global/racecourse');
|
||||
return Course.array().parseAsync(await resp.json());
|
||||
}
|
||||
|
||||
export interface SectionInfo {
|
||||
section: number;
|
||||
phase: race.Phase;
|
||||
endRate: number;
|
||||
posKeep?: boolean;
|
||||
rush?: boolean;
|
||||
spotStruggleEnter?: boolean;
|
||||
}
|
||||
|
||||
export const SECTIONS: ReadonlyArray<SectionInfo> = [
|
||||
{ section: 1, phase: race.Phase.EarlyRace, endRate: 1 / 24, posKeep: true },
|
||||
{ section: 2, phase: race.Phase.EarlyRace, endRate: 2 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
|
||||
{ section: 3, phase: race.Phase.EarlyRace, endRate: 3 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
|
||||
{ section: 4, phase: race.Phase.EarlyRace, endRate: 4 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
|
||||
{ section: 5, phase: race.Phase.MidRace, endRate: 5 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
|
||||
{ section: 6, phase: race.Phase.MidRace, endRate: 6 / 24, posKeep: true, rush: true },
|
||||
{ section: 7, phase: race.Phase.MidRace, endRate: 7 / 24, posKeep: true, rush: true },
|
||||
{ section: 8, phase: race.Phase.MidRace, endRate: 8 / 24, posKeep: true, rush: true },
|
||||
{ section: 9, phase: race.Phase.MidRace, endRate: 9 / 24, posKeep: true, rush: true },
|
||||
{ section: 10, phase: race.Phase.MidRace, endRate: 10 / 24, posKeep: true },
|
||||
{ section: 11, phase: race.Phase.MidRace, endRate: 11 / 24 },
|
||||
{ section: 12, phase: race.Phase.MidRace, endRate: 12 / 24 },
|
||||
{ section: 13, phase: race.Phase.MidRace, endRate: 13 / 24 },
|
||||
{ section: 14, phase: race.Phase.MidRace, endRate: 14 / 24 },
|
||||
{ section: 15, phase: race.Phase.MidRace, endRate: 15 / 24 },
|
||||
{ section: 16, phase: race.Phase.MidRace, endRate: 16 / 24 },
|
||||
{ section: 17, phase: race.Phase.LateRace, endRate: 17 / 24 },
|
||||
{ section: 18, phase: race.Phase.LateRace, endRate: 18 / 24 },
|
||||
{ section: 19, phase: race.Phase.LateRace, endRate: 19 / 24 },
|
||||
{ section: 20, phase: race.Phase.LateRace, endRate: 20 / 24 },
|
||||
{ section: 21, phase: race.Phase.LastSpurt, endRate: 21 / 24 },
|
||||
{ section: 22, phase: race.Phase.LastSpurt, endRate: 22 / 24 },
|
||||
{ section: 23, phase: race.Phase.LastSpurt, endRate: 23 / 24 },
|
||||
{ section: 24, phase: race.Phase.LastSpurt, endRate: 24 / 24 },
|
||||
];
|
||||
|
||||
const dirtOnly = [10101, 10103, 10104, 10105];
|
||||
|
||||
export function referName(course: Course): string {
|
||||
const parts = [course.race_track_name, course.distance.toString()];
|
||||
if (course.ground === race.Surface.Dirt && !dirtOnly.includes(course.race_track_id)) {
|
||||
parts.push('Dirt');
|
||||
}
|
||||
switch (course.inout) {
|
||||
case InOut.None:
|
||||
break;
|
||||
case InOut.Inner:
|
||||
parts.push('(Inner)');
|
||||
break;
|
||||
case InOut.Outer:
|
||||
parts.push('(Outer)');
|
||||
break;
|
||||
case InOut.OuterToInner:
|
||||
parts.push('(Outer→Inner)');
|
||||
break;
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export function fillSlopes(slopes: Slope[], distance: number): Slope[] {
|
||||
const r: Slope[] = [{ start: 0, end: slopes[0]?.start ?? distance, per: 0 }];
|
||||
for (const [i, s] of slopes.entries()) {
|
||||
r.push(s);
|
||||
r.push({ start: s.end, end: slopes[i + 1]?.start ?? distance, per: 0 });
|
||||
}
|
||||
return r.filter((s) => s.start < s.end);
|
||||
}
|
||||
|
||||
export enum DistanceType {
|
||||
Sprint = 1,
|
||||
Mile = 2,
|
||||
Medium = 3,
|
||||
Long = 4,
|
||||
}
|
||||
|
||||
export function distanceType(course: Course): DistanceType {
|
||||
if (course.distance < 1500) {
|
||||
return DistanceType.Sprint;
|
||||
}
|
||||
if (course.distance < 1900) {
|
||||
return DistanceType.Mile;
|
||||
}
|
||||
if (course.distance < 2500) {
|
||||
return DistanceType.Medium;
|
||||
}
|
||||
return DistanceType.Long;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import * as condition from './condition';
|
||||
|
||||
test.for([
|
||||
{
|
||||
c: '',
|
||||
want: [],
|
||||
},
|
||||
{
|
||||
c: 'all_corner_random==1',
|
||||
want: [[{ type: 'all_corner_random', op: '==', arg: 1 }]],
|
||||
},
|
||||
{
|
||||
c: 'corner!=0',
|
||||
want: [[{ type: 'corner', op: '!=', arg: 0 }]],
|
||||
},
|
||||
{
|
||||
c: 'order_rate<80',
|
||||
want: [[{ type: 'order_rate', op: '<', arg: 80 }]],
|
||||
},
|
||||
{
|
||||
c: 'distance_diff_top<=5',
|
||||
want: [[{ type: 'distance_diff_top', op: '<=', arg: 5 }]],
|
||||
},
|
||||
{
|
||||
c: 'motivation>=4',
|
||||
want: [[{ type: 'motivation', op: '>=', arg: 4 }]],
|
||||
},
|
||||
{
|
||||
c: 'compete_fight_count>0',
|
||||
want: [[{ type: 'compete_fight_count', op: '>', arg: 0 }]],
|
||||
},
|
||||
{
|
||||
c: 'phase==1&change_order_onetime<0',
|
||||
want: [
|
||||
[
|
||||
{ type: 'phase', op: '==', arg: 1 },
|
||||
{ type: 'change_order_onetime', op: '<', arg: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
c: 'temptation_count==0&remain_distance<=201&remain_distance>=199&order>=4&order_rate<=70',
|
||||
want: [
|
||||
[
|
||||
{ type: 'temptation_count', op: '==', arg: 0 },
|
||||
{ type: 'remain_distance', op: '<=', arg: 201 },
|
||||
{ type: 'remain_distance', op: '>=', arg: 199 },
|
||||
{ type: 'order', op: '>=', arg: 4 },
|
||||
{ type: 'order_rate', op: '<=', arg: 70 },
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
c: 'track_id==10001@track_id==10002@track_id==10004@track_id==10010@track_id==10103@track_id==10104',
|
||||
want: [
|
||||
[{ type: 'track_id', op: '==', arg: 10001 }],
|
||||
[{ type: 'track_id', op: '==', arg: 10002 }],
|
||||
[{ type: 'track_id', op: '==', arg: 10004 }],
|
||||
[{ type: 'track_id', op: '==', arg: 10010 }],
|
||||
[{ type: 'track_id', op: '==', arg: 10103 }],
|
||||
[{ type: 'track_id', op: '==', arg: 10104 }],
|
||||
],
|
||||
},
|
||||
{
|
||||
c: 'distance_type==1&phase>=2&order_rate>=50&is_overtake==1@distance_type==2&phase>=2&order_rate>=50&is_overtake==1',
|
||||
want: [
|
||||
[
|
||||
{ type: 'distance_type', op: '==', arg: 1 },
|
||||
{ type: 'phase', op: '>=', arg: 2 },
|
||||
{ type: 'order_rate', op: '>=', arg: 50 },
|
||||
{ type: 'is_overtake', op: '==', arg: 1 },
|
||||
],
|
||||
[
|
||||
{ type: 'distance_type', op: '==', arg: 2 },
|
||||
{ type: 'phase', op: '>=', arg: 2 },
|
||||
{ type: 'order_rate', op: '>=', arg: 50 },
|
||||
{ type: 'is_overtake', op: '==', arg: 1 },
|
||||
],
|
||||
],
|
||||
},
|
||||
])('$c', ({ c, want }) => expect(condition.parse(c)).toEqual(want));
|
||||
@@ -0,0 +1,344 @@
|
||||
import * as race from '$lib/race';
|
||||
import * as racecourse from '$lib/data/racecourse';
|
||||
|
||||
export enum Op {
|
||||
Eq = '==',
|
||||
Ne = '!=',
|
||||
Le = '<=',
|
||||
Lt = '<',
|
||||
Ge = '>=',
|
||||
Gt = '>',
|
||||
}
|
||||
|
||||
export interface Condition {
|
||||
type: string;
|
||||
op: Op;
|
||||
arg: number;
|
||||
}
|
||||
|
||||
const re = /(?<type>[a-z0-9_]+)(?<op>[=!<>]=?)(?<arg>-?\d+)/;
|
||||
|
||||
function parseCond(conj: string): Condition {
|
||||
// Using ! assertions because we want to explode if the parse fails.
|
||||
const r = re.exec(conj)!.groups!;
|
||||
return { type: r.type, op: r.op as Op, arg: parseInt(r.arg) };
|
||||
}
|
||||
|
||||
export function parse(cond: string): Condition[][] {
|
||||
if (cond === '') {
|
||||
return [];
|
||||
}
|
||||
return cond.split('@').map((disj) => disj.split('&').map(parseCond));
|
||||
}
|
||||
|
||||
export enum SpanType {
|
||||
Never = 'N/A',
|
||||
Random = 'Random',
|
||||
Skill = 'Triggered',
|
||||
Whenever = 'Immediate',
|
||||
}
|
||||
|
||||
export interface Span {
|
||||
start: number;
|
||||
end: number;
|
||||
type: SpanType;
|
||||
}
|
||||
|
||||
function coursespans(spans: racecourse.Span[] | racecourse.Slope[], type: SpanType): Span[] {
|
||||
return or(spans.map((s) => ({ start: s.start, end: s.end, type })));
|
||||
}
|
||||
|
||||
function phase(course: racecourse.Course, op: Op, phase: race.Phase, type: SpanType): Span {
|
||||
const p = race.phases(course.distance);
|
||||
switch (op) {
|
||||
case Op.Eq:
|
||||
if (phase === race.Phase.EarlyRace) {
|
||||
return { start: 0, end: p[race.Phase.EarlyRace], type };
|
||||
}
|
||||
return { start: p[phase - 1 as race.Phase], end: p[phase], type };
|
||||
case Op.Le:
|
||||
return { start: 0, end: p[phase], type };
|
||||
case Op.Ge:
|
||||
if (phase === race.Phase.EarlyRace) {
|
||||
return { start: 0, end: course.distance, type };
|
||||
}
|
||||
return { start: p[phase - 1 as race.Phase], end: course.distance, type };
|
||||
default:
|
||||
// Other operators aren't used.
|
||||
throw new Error("weird operator on phase condition");
|
||||
}
|
||||
}
|
||||
|
||||
function endafter(course: racecourse.Course, type: SpanType): Span {
|
||||
return phase(course, Op.Ge, race.Phase.LateRace, type);
|
||||
}
|
||||
|
||||
function is_finalcorner(course: racecourse.Course, toEnd: boolean, type: SpanType): Span {
|
||||
const c = course.spans.findLast((s) => s.corner != null);
|
||||
if (c == null) {
|
||||
return { start: 0, end: 0, type: SpanType.Never };
|
||||
}
|
||||
return { start: c.start, end: toEnd ? course.distance : c.end, type };
|
||||
}
|
||||
|
||||
function laststraight(course: racecourse.Course, type: SpanType): Span {
|
||||
const s = course.spans[course.spans.length - 1];
|
||||
return { start: s.start, end: s.end, type };
|
||||
}
|
||||
|
||||
function distrate(course: racecourse.Course, cond: Condition, type: SpanType): Span {
|
||||
switch (cond.op) {
|
||||
case Op.Le:
|
||||
case Op.Lt:
|
||||
return { start: 0, end: Math.round(course.distance * cond.arg / 100), type };
|
||||
case Op.Ge:
|
||||
case Op.Gt:
|
||||
return { start: Math.round(course.distance * cond.arg / 100), end: course.distance, type };
|
||||
}
|
||||
return { start: 0, end: 0, type: SpanType.Never };
|
||||
}
|
||||
|
||||
function remaining(course: racecourse.Course, cond: Condition, type: SpanType): Span {
|
||||
switch (cond.op) {
|
||||
case Op.Eq:
|
||||
return { start: course.distance - cond.arg - 1, end: course.distance - cond.arg + 1, type };
|
||||
case Op.Ne:
|
||||
throw new Error("weird operator on remaining distance");
|
||||
case Op.Le:
|
||||
case Op.Lt:
|
||||
return { start: course.distance - cond.arg, end: course.distance, type };
|
||||
case Op.Ge:
|
||||
case Op.Gt:
|
||||
return { start: 0, end: course.distance - cond.arg, type };
|
||||
}
|
||||
}
|
||||
|
||||
function later(s: Span, p: number): Span {
|
||||
return { ...s, start: (s.start + s.end) * p };
|
||||
}
|
||||
|
||||
function first(s: Span, p: number): Span {
|
||||
return { ...s, end: (s.start + s.end) * p };
|
||||
}
|
||||
|
||||
function accumulatetime(course: racecourse.Course, t: number, type: SpanType): Span {
|
||||
return { start: race.baseSpeed(course.distance) * t, end: course.distance, type };
|
||||
}
|
||||
|
||||
function intersection(a: Span, b: Span): Span {
|
||||
const start = Math.max(a.start, b.start);
|
||||
const end = Math.min(a.end, b.end);
|
||||
if (start >= end) {
|
||||
return { start: 0, end: 0, type: SpanType.Never };
|
||||
}
|
||||
switch (a.type) {
|
||||
case SpanType.Never:
|
||||
return { start: 0, end: 0, type: SpanType.Never };
|
||||
case SpanType.Random:
|
||||
case SpanType.Skill:
|
||||
if (b.type === SpanType.Never) {
|
||||
return { start: 0, end: 0, type: SpanType.Never };
|
||||
}
|
||||
// We use a.type on the assumption that we will never intersect
|
||||
// a random span with a skill-triggered span.
|
||||
return { start, end, type: a.type };
|
||||
case SpanType.Whenever:
|
||||
return { start, end, type: b.type };
|
||||
}
|
||||
}
|
||||
|
||||
function opcompare(cond: Condition, x: number) {
|
||||
switch (cond.op) {
|
||||
case Op.Eq:
|
||||
return x === cond.arg;
|
||||
case Op.Ne:
|
||||
return x !== cond.arg;
|
||||
case Op.Le:
|
||||
return x <= cond.arg;
|
||||
case Op.Lt:
|
||||
return x < cond.arg;
|
||||
case Op.Ge:
|
||||
return x >= cond.arg;
|
||||
case Op.Gt:
|
||||
return x > cond.arg;
|
||||
}
|
||||
}
|
||||
|
||||
function must(x: boolean): SpanType {
|
||||
return x ? SpanType.Whenever : SpanType.Never;
|
||||
}
|
||||
|
||||
const perCond: Readonly<Partial<Record<Condition['type'], (course: racecourse.Course, cond: Condition) => Span[]>>> = {
|
||||
'accumulatetime': (course, cond) => [accumulatetime(course, cond.arg, SpanType.Whenever)],
|
||||
'activate_count_all_team': (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
|
||||
'activate_count_end_after': (course) => [endafter(course, SpanType.Skill)],
|
||||
'activate_count_heal': (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
|
||||
'activate_count_later_half': (course) => [{ start: course.distance / 2, end: course.distance, type: SpanType.Skill }],
|
||||
'activate_count_middle': (course) => [phase(course, Op.Eq, race.Phase.MidRace, SpanType.Skill)],
|
||||
'activate_count_start': (course) => [phase(course, Op.Eq, race.Phase.EarlyRace, SpanType.Skill)],
|
||||
'all_corner_random': (course) => coursespans(course.spans.filter((s) => s.corner != null), SpanType.Random),
|
||||
'change_order_up_end_after': (course) => [endafter(course, SpanType.Whenever)],
|
||||
'change_order_up_finalcorner_after': (course) => [is_finalcorner(course, true, SpanType.Whenever)],
|
||||
'change_order_up_middle': (course) => [phase(course, Op.Eq, race.Phase.MidRace, SpanType.Whenever)],
|
||||
'compete_fight_count': (course) => [laststraight(course, SpanType.Whenever)],
|
||||
'corner': (course, cond) => coursespans(course.spans.filter((s) => opcompare(cond, s.corner ?? 0)), SpanType.Whenever),
|
||||
'corner_count': (course, cond) => [{ start: 0, end: course.distance, type: opcompare(cond, course.spans.reduce((n, s) => n + (s.corner != null ? 1 : 0), 0)) ? SpanType.Never : SpanType.Whenever }],
|
||||
'corner_random': (course, cond) => {
|
||||
const c = course.spans.findLast((s) => s.corner === cond.arg);
|
||||
if (c == null) {
|
||||
return [{ start: 0, end: 0, type: SpanType.Never }];
|
||||
}
|
||||
return [{ start: c.start, end: c.end, type: SpanType.Random }];
|
||||
},
|
||||
'course_distance': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.distance)) }],
|
||||
'distance_rate': (course, cond) => [distrate(course, cond, SpanType.Whenever)],
|
||||
'distance_rate_after_random': (course, cond) => [distrate(course, cond, SpanType.Random)],
|
||||
'distance_type': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, racecourse.distanceType(course))) }],
|
||||
'down_slope_random': (course) => coursespans(course.slopes.filter((s) => s.per < 0), SpanType.Random),
|
||||
'furlong': (course, cond) => {
|
||||
switch (cond.op) {
|
||||
case Op.Eq:
|
||||
return [{ start: cond.arg * 201.168, end: (cond.arg + 1) * 201.168, type: SpanType.Whenever }];
|
||||
case Op.Ne:
|
||||
return [
|
||||
{ start: 0, end: cond.arg * 201.168, type: SpanType.Whenever },
|
||||
{ start: (cond.arg + 1) * 201.168, end: course.distance, type: SpanType.Whenever },
|
||||
];
|
||||
case Op.Le:
|
||||
case Op.Lt:
|
||||
return [{ start: 0, end: (cond.arg + 1) * 201.168, type: SpanType.Whenever }];
|
||||
case Op.Ge:
|
||||
case Op.Gt:
|
||||
return [{ start: cond.arg * 201.168, end: course.distance, type: SpanType.Whenever }];
|
||||
}
|
||||
},
|
||||
'ground_type': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.ground)) }],
|
||||
// hp_per would be an interesting challenge...
|
||||
'is_finalcorner': (course, cond) => {
|
||||
// Either is_finalcorner==1 or is_finalcorner==0.
|
||||
if (cond.arg === 1) {
|
||||
return [is_finalcorner(course, true, SpanType.Whenever)];
|
||||
}
|
||||
const c = course.spans.findLast((s) => s.corner != null);
|
||||
if (c == null) {
|
||||
return [{ start: 0, end: course.distance, type: SpanType.Whenever }];
|
||||
}
|
||||
return [{ start: 0, end: c.start, type: SpanType.Whenever }];
|
||||
},
|
||||
// TODO(zeph): is_abroad
|
||||
'is_activate_any_skill': (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
|
||||
'is_activate_heal_skill': (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
|
||||
'is_basis_distance': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.distance % 400 === 0 ? 1 : 0)) }],
|
||||
'is_dirtgrade': (course, cond) => {
|
||||
// NAR tracks: Ooi, Kawasaki, Funabashi, Morioka
|
||||
const is = [10101, 10103, 10104, 10105].includes(course.race_track_id);
|
||||
return [{ start: 0, end: course.distance, type: must(opcompare(cond, is ? 1 : 0)) }];
|
||||
},
|
||||
'is_finalcorner_laterhalf': (course) => [later(is_finalcorner(course, false, SpanType.Whenever), 0.5)],
|
||||
'is_finalcorner_random': (course) => [is_finalcorner(course, false, SpanType.Random)],
|
||||
'is_last_straight': (course) => [laststraight(course, SpanType.Whenever)],
|
||||
'is_last_straight_onetime': (course) => [first(laststraight(course, SpanType.Whenever), 0.02)],
|
||||
'is_lastspurt': (course) => [endafter(course, SpanType.Whenever)],
|
||||
// could implement is_temptation!=0, but that condition isn't used
|
||||
'is_tight_track': (course, cond) => {
|
||||
// Sapporo, Hakodate, Fukushima, Kokura, Kawasaki, Funabashi
|
||||
// TODO(zeph): GameTora says "afaik" for this list...
|
||||
const is = [10001, 10002, 10004, 10010, 10103, 10104];
|
||||
return [{ start: 0, end: course.distance, type: must(opcompare(cond, is ? 1 : 0)) }];
|
||||
},
|
||||
'last_straight_random': (course) => [laststraight(course, SpanType.Random)],
|
||||
'lastspurt': (course) => [endafter(course, SpanType.Whenever)],
|
||||
'order_rate_in20_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_in40_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_in50_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_in80_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_out20_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_out40_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_out50_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'order_rate_out70_continue': (course) => [accumulatetime(course, 5, SpanType.Whenever)],
|
||||
'phase': (course, cond) => [phase(course, cond.op, cond.arg, SpanType.Whenever)],
|
||||
'phase_corner_random': (course, cond) => {
|
||||
const p = phase(course, Op.Eq, cond.arg, SpanType.Random);
|
||||
return coursespans(course.spans.filter((s) => s.corner != null), SpanType.Random).map((s) => intersection(s, p));
|
||||
},
|
||||
'phase_first_half_straight_random': (course, cond) => {
|
||||
const p = first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5);
|
||||
return coursespans(course.spans.filter((s) => s.straight != null), SpanType.Random).map((s) => intersection(s, p));
|
||||
},
|
||||
'phase_firsthalf': (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.5)],
|
||||
'phase_firsthalf_random': (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5)],
|
||||
'phase_firstquarter': (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.25)],
|
||||
'phase_firstquarter_random': (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.25)],
|
||||
'phase_laterhalf': (course, cond) => [later(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.5)],
|
||||
'phase_laterhalf_random': (course, cond) => [later(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5)],
|
||||
'phase_latter_half_straight_random': (course, cond) => {
|
||||
const p = later(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5);
|
||||
return coursespans(course.spans.filter((s) => s.straight != null), SpanType.Random).map((s) => intersection(s, p));
|
||||
},
|
||||
'phase_random': (course, cond) => [phase(course, cond.op, cond.arg, SpanType.Random)],
|
||||
'phase_straight_random': (course, cond) => {
|
||||
const p = phase(course, Op.Eq, cond.arg, SpanType.Random);
|
||||
return coursespans(course.spans.filter((s) => s.straight != null), SpanType.Random).map((s) => intersection(s, p));
|
||||
},
|
||||
'remain_distance': (course, cond) => [remaining(course, cond, SpanType.Whenever)],
|
||||
'remain_distance_viewer_id': (course, cond) => [remaining(course, cond, SpanType.Whenever)],
|
||||
'rotation': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.turn)) }],
|
||||
'run_at_full_speed_random': (course) => [endafter(course, SpanType.Random)],
|
||||
'slope': (course, cond) => {
|
||||
switch (cond.arg) {
|
||||
case 0:
|
||||
return coursespans(racecourse.fillSlopes(course.slopes, course.distance).filter((s) => s.per === 0), SpanType.Whenever);
|
||||
case 1:
|
||||
return coursespans(course.slopes.filter((s) => s.per > 0), SpanType.Whenever);
|
||||
case 2:
|
||||
return coursespans(course.slopes.filter((s) => s.per < 0), SpanType.Whenever);
|
||||
default:
|
||||
throw new Error("weird argument to slope condition");
|
||||
}
|
||||
},
|
||||
'straight_front_type': (course, cond) => coursespans(course.spans.filter((s) => s.straight === cond.arg), SpanType.Whenever),
|
||||
'straight_random': (course) => coursespans(course.spans.filter((s) => s.straight != null), SpanType.Random),
|
||||
'track_id': (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.race_track_id)) }],
|
||||
'up_slope_random': (course) => coursespans(course.slopes.filter((s) => s.per > 0), SpanType.Random),
|
||||
'up_slope_random_later_half': (course) => {
|
||||
const p = { start: course.distance / 2, end: course.distance, type: SpanType.Random };
|
||||
return coursespans(course.slopes.filter((s) => s.per > 0), SpanType.Random).map((s) => intersection(s, p));
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function spans(course: racecourse.Course, cond: Condition): Span[] {
|
||||
const f = perCond[cond.type];
|
||||
return f?.(course, cond) ?? [{ start: 0, end: course.distance, type: SpanType.Whenever }];
|
||||
}
|
||||
|
||||
export function and(as: Span[], bs: Span[]): Span[] {
|
||||
const a = as.toSorted((l, r) => l.start - r.start);
|
||||
const b = bs.toSorted((l, r) => l.start - r.start);
|
||||
// TODO(zeph): this is quadratic in the number of spans; we could do it linear
|
||||
return a.flatMap((u) => b.map((v) => intersection(u, v)).filter((v) => v.type !== SpanType.Never));
|
||||
}
|
||||
|
||||
export function or(spans: Span[]): Span[] {
|
||||
const a = spans.filter((s) => s.type !== SpanType.Never).sort((l, r) => l.start - r.start);
|
||||
if (spans.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const r: Span[] = [];
|
||||
let cur = a.shift()!;
|
||||
while (a.length > 0) {
|
||||
const next = a.shift()!;
|
||||
// If they're disjoint or different types, they can't be merged.
|
||||
// Note that we only need to check next start versus current end
|
||||
// because we sort by start position first.
|
||||
if (next.start > cur.end || next.type !== cur.type) {
|
||||
r.push(cur);
|
||||
cur = next;
|
||||
continue;
|
||||
}
|
||||
// The current start is always the minimum of the two because we sorted,
|
||||
// and the merged type is the same as the current type.
|
||||
cur.end = Math.max(cur.end, next.end);
|
||||
}
|
||||
r.push(cur);
|
||||
return r;
|
||||
}
|
||||
Reference in New Issue
Block a user