mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-04-11 16:01:56 +00:00
436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
import { describe, expect, it } from "vitest"
|
|
import { getMatchStateConfig } from "match-state-config"
|
|
import {
|
|
accumulatedMatchSeconds,
|
|
activeClockForPhase,
|
|
adjustClockState,
|
|
applyChoiceAction,
|
|
applyMatchAction,
|
|
buildInitialState,
|
|
deleteEventFromState,
|
|
derivedStats,
|
|
hasRunningClockOrTimer,
|
|
matchResultDefaults,
|
|
moveToNextPhase,
|
|
moveToPreviousPhase,
|
|
recordProgressiveAction,
|
|
recomputeDerivedState,
|
|
scoreboardStatePayload,
|
|
startAuxiliaryTimerState,
|
|
startClockState,
|
|
stopAuxiliaryTimerState,
|
|
stopClockState,
|
|
stopAllAuxiliaryTimers,
|
|
swapEventParticipants,
|
|
syncClockSnapshot,
|
|
swapPhaseParticipants
|
|
} from "match-state-engine"
|
|
|
|
function buildEvent(overrides = {}) {
|
|
return {
|
|
id: 1,
|
|
phaseKey: "period_1",
|
|
phaseLabel: "Period 1",
|
|
clockSeconds: 120,
|
|
participantKey: "w1",
|
|
actionKey: "takedown_3",
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe("match state engine", () => {
|
|
it("replays takedown and escape into score and control", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
state.events = [
|
|
buildEvent(),
|
|
buildEvent({
|
|
id: 2,
|
|
participantKey: "w2",
|
|
actionKey: "escape_1",
|
|
clockSeconds: 80
|
|
})
|
|
]
|
|
|
|
recomputeDerivedState(config, state)
|
|
|
|
expect(state.participantScores).toEqual({ w1: 3, w2: 1 })
|
|
expect(state.control).toBe("neutral")
|
|
expect(state.displayControl).toBe("neutral")
|
|
})
|
|
|
|
it("stores non defer choices and applies chosen starting control to later periods", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
state.phaseIndex = 2
|
|
state.events = [
|
|
buildEvent({
|
|
id: 1,
|
|
phaseKey: "choice_1",
|
|
phaseLabel: "Choice 1",
|
|
clockSeconds: 0,
|
|
participantKey: "w1",
|
|
actionKey: "choice_top"
|
|
})
|
|
]
|
|
|
|
recomputeDerivedState(config, state)
|
|
|
|
expect(state.selections.choice_1).toEqual({ participantKey: "w1", choiceKey: "top" })
|
|
expect(state.control).toBe("w1_control")
|
|
expect(state.displayControl).toBe("w1_control")
|
|
})
|
|
|
|
it("ignores defer as a final selection", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
state.phaseIndex = 2
|
|
state.events = [
|
|
buildEvent({
|
|
id: 1,
|
|
phaseKey: "choice_1",
|
|
phaseLabel: "Choice 1",
|
|
clockSeconds: 0,
|
|
participantKey: "w1",
|
|
actionKey: "choice_defer"
|
|
})
|
|
]
|
|
|
|
recomputeDerivedState(config, state)
|
|
|
|
expect(state.selections).toEqual({})
|
|
expect(state.control).toBe("neutral")
|
|
expect(state.displayControl).toBe("neutral")
|
|
})
|
|
|
|
it("derives legacy stats grouped by period", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const stats = derivedStats(config, [
|
|
buildEvent(),
|
|
buildEvent({
|
|
id: 2,
|
|
participantKey: "w2",
|
|
actionKey: "escape_1",
|
|
clockSeconds: 80
|
|
}),
|
|
buildEvent({
|
|
id: 3,
|
|
phaseKey: "choice_1",
|
|
phaseLabel: "Choice 1",
|
|
clockSeconds: 0,
|
|
participantKey: "w1",
|
|
actionKey: "choice_defer"
|
|
})
|
|
])
|
|
|
|
expect(stats.w1).toBe("Period 1: T3\nChoice 1: |Deferred|")
|
|
expect(stats.w2).toBe("Period 1: E1")
|
|
})
|
|
|
|
it("derives accumulated match time from period clocks", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
state.phaseIndex = 2
|
|
state.clocksByPhase.period_1.remainingSeconds = 42
|
|
state.clocksByPhase.period_2.remainingSeconds = 75
|
|
state.clocksByPhase.period_3.remainingSeconds = 120
|
|
|
|
const total = accumulatedMatchSeconds(config, state, "period_2")
|
|
|
|
expect(total).toBe((120 - 42) + (120 - 75))
|
|
})
|
|
|
|
it("builds scoreboard payload from canonical state and metadata", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.participantScores = { w1: 6, w2: 2 }
|
|
state.phaseIndex = 2
|
|
|
|
const payload = scoreboardStatePayload(config, state, {
|
|
tournamentId: 1,
|
|
boutNumber: 1001,
|
|
weightLabel: "106",
|
|
ruleset: "folkstyle_usa",
|
|
bracketPosition: "Bracket Round of 64",
|
|
w1Name: "Wrestler 1",
|
|
w2Name: "Wrestler 2",
|
|
w1School: "School A",
|
|
w2School: "School B"
|
|
})
|
|
|
|
expect(payload.participantScores).toEqual({ w1: 6, w2: 2 })
|
|
expect(payload.phaseIndex).toBe(2)
|
|
expect(payload.metadata.boutNumber).toBe(1001)
|
|
expect(payload.metadata.w1Name).toBe("Wrestler 1")
|
|
expect(payload.matchResult).toEqual({ finished: false })
|
|
})
|
|
|
|
it("records progressive penalty with linked awarded points", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.nextEventId = 1
|
|
state.nextEventGroupId = 1
|
|
|
|
const buildControllerStyleEvent = (participantKey, actionKey, options = {}) => ({
|
|
id: state.nextEventId++,
|
|
phaseKey: "period_1",
|
|
phaseLabel: "Period 1",
|
|
clockSeconds: 120,
|
|
participantKey,
|
|
actionKey,
|
|
actionGroupId: options.actionGroupId
|
|
})
|
|
|
|
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
|
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
|
recordProgressiveAction(config, state, "w1", "penalty", buildControllerStyleEvent)
|
|
|
|
expect(state.events.map((eventRecord) => [eventRecord.participantKey, eventRecord.actionKey])).toEqual([
|
|
["w1", "penalty"],
|
|
["w2", "plus_1"],
|
|
["w1", "penalty"],
|
|
["w2", "plus_1"],
|
|
["w1", "penalty"],
|
|
["w2", "plus_2"]
|
|
])
|
|
})
|
|
|
|
it("applies a normal match action by creating one event", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
const applied = applyMatchAction(config, state, config.phaseSequence[0], 118, "w1", "takedown_3")
|
|
|
|
expect(applied).toBe(true)
|
|
expect(state.events).toHaveLength(1)
|
|
expect(state.events[0]).toMatchObject({
|
|
phaseKey: "period_1",
|
|
clockSeconds: 118,
|
|
participantKey: "w1",
|
|
actionKey: "takedown_3"
|
|
})
|
|
})
|
|
|
|
it("applies a progressive action by creating offense and linked award events when earned", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
|
|
expect(applyMatchAction(config, state, config.phaseSequence[0], 118, "w1", "stalling")).toBe(true)
|
|
expect(applyMatchAction(config, state, config.phaseSequence[0], 110, "w1", "stalling")).toBe(true)
|
|
|
|
expect(state.events.map((eventRecord) => [eventRecord.participantKey, eventRecord.actionKey])).toEqual([
|
|
["w1", "stalling"],
|
|
["w1", "stalling"],
|
|
["w2", "plus_1"]
|
|
])
|
|
})
|
|
|
|
it("applies a defer choice without storing a final selection", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
const choicePhase = config.phaseSequence[1]
|
|
|
|
const result = applyChoiceAction(state, choicePhase, 0, "w1", "defer")
|
|
|
|
expect(result).toEqual({ applied: true, deferred: true })
|
|
expect(state.events).toHaveLength(1)
|
|
expect(state.events[0].actionKey).toBe("choice_defer")
|
|
expect(state.selections).toEqual({})
|
|
})
|
|
|
|
it("applies a non defer choice and stores the selection", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
const choicePhase = config.phaseSequence[1]
|
|
|
|
const result = applyChoiceAction(state, choicePhase, 0, "w2", "bottom")
|
|
|
|
expect(result).toEqual({ applied: true, deferred: false })
|
|
expect(state.events[0].actionKey).toBe("choice_bottom")
|
|
expect(state.selections.choice_1).toEqual({ participantKey: "w2", choiceKey: "bottom" })
|
|
})
|
|
|
|
it("deleting a timer-used event restores timer time", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.timers.w1.injury.remainingSeconds = 60
|
|
state.events = [
|
|
buildEvent({
|
|
id: 1,
|
|
participantKey: "w1",
|
|
actionKey: "timer_used_injury",
|
|
elapsedSeconds: 20
|
|
})
|
|
]
|
|
|
|
const deleted = deleteEventFromState(config, state, 1)
|
|
|
|
expect(deleted).toBe(true)
|
|
expect(state.events).toEqual([])
|
|
expect(state.timers.w1.injury.remainingSeconds).toBe(80)
|
|
})
|
|
|
|
it("swapping a timer-used event moves the used time to the other wrestler", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.timers.w1.blood.remainingSeconds = 240
|
|
state.timers.w2.blood.remainingSeconds = 260
|
|
state.events = [
|
|
buildEvent({
|
|
id: 1,
|
|
participantKey: "w1",
|
|
actionKey: "timer_used_blood",
|
|
elapsedSeconds: 30
|
|
})
|
|
]
|
|
|
|
const swapped = swapEventParticipants(config, state, 1)
|
|
|
|
expect(swapped).toBe(true)
|
|
expect(state.events[0].participantKey).toBe("w2")
|
|
expect(state.timers.w1.blood.remainingSeconds).toBe(270)
|
|
expect(state.timers.w2.blood.remainingSeconds).toBe(230)
|
|
})
|
|
|
|
it("swapping a whole period flips all participants in that period", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.events = [
|
|
buildEvent({
|
|
id: 1,
|
|
participantKey: "w1",
|
|
actionKey: "takedown_3"
|
|
}),
|
|
buildEvent({
|
|
id: 2,
|
|
participantKey: "w2",
|
|
actionKey: "escape_1",
|
|
clockSeconds: 80
|
|
}),
|
|
buildEvent({
|
|
id: 3,
|
|
phaseKey: "choice_1",
|
|
phaseLabel: "Choice 1",
|
|
participantKey: "w1",
|
|
actionKey: "choice_defer",
|
|
clockSeconds: 0
|
|
})
|
|
]
|
|
|
|
const swapped = swapPhaseParticipants(config, state, "period_1")
|
|
|
|
expect(swapped).toBe(true)
|
|
expect(state.events.slice(0, 2).map((eventRecord) => eventRecord.participantKey)).toEqual(["w2", "w1"])
|
|
expect(state.events[2].participantKey).toBe("w1")
|
|
})
|
|
|
|
it("starts, stops, and adjusts a running match clock", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
const activeClock = state.clocksByPhase.period_1
|
|
|
|
expect(startClockState(activeClock, 1_000)).toBe(true)
|
|
expect(activeClock.running).toBe(true)
|
|
expect(activeClock.startedAt).toBe(1_000)
|
|
|
|
expect(adjustClockState(activeClock, -10, 6_000)).toBe(true)
|
|
expect(activeClock.remainingSeconds).toBe(105)
|
|
expect(activeClock.startedAt).toBe(6_000)
|
|
|
|
expect(stopClockState(activeClock, 11_000)).toBe(true)
|
|
expect(activeClock.running).toBe(false)
|
|
expect(activeClock.remainingSeconds).toBe(100)
|
|
expect(syncClockSnapshot(activeClock)).toEqual({
|
|
durationSeconds: 120,
|
|
remainingSeconds: 100,
|
|
running: false,
|
|
startedAt: null
|
|
})
|
|
})
|
|
|
|
it("starts and stops an auxiliary timer with elapsed time", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const timerState = buildInitialState(config).timers.w1.injury
|
|
|
|
expect(startAuxiliaryTimerState(timerState, 2_000)).toBe(true)
|
|
const result = stopAuxiliaryTimerState(timerState, 17_000)
|
|
|
|
expect(result).toEqual({ stopped: true, elapsedSeconds: 15 })
|
|
expect(timerState.running).toBe(false)
|
|
expect(timerState.remainingSeconds).toBe(75)
|
|
})
|
|
|
|
it("derives match result defaults from score and overtime context", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.participantScores = { w1: 4, w2: 2 }
|
|
|
|
const defaults = matchResultDefaults(state, {
|
|
w1Id: 11,
|
|
w2Id: 22,
|
|
currentPhase: config.phaseSequence.find((phase) => phase.key === "sv_1"),
|
|
accumulationSeconds: 83
|
|
})
|
|
|
|
expect(defaults).toEqual({
|
|
winnerId: 11,
|
|
overtimeType: "SV-1",
|
|
winnerScore: 4,
|
|
loserScore: 2,
|
|
pinMinutes: 1,
|
|
pinSeconds: 23
|
|
})
|
|
})
|
|
|
|
it("moves between phases and resets control to the new phase base", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.control = "w1_control"
|
|
|
|
expect(moveToNextPhase(config, state)).toBe(true)
|
|
expect(state.phaseIndex).toBe(1)
|
|
|
|
state.selections.choice_1 = { participantKey: "w2", choiceKey: "bottom" }
|
|
expect(moveToNextPhase(config, state)).toBe(true)
|
|
expect(state.phaseIndex).toBe(2)
|
|
expect(state.control).toBe("w1_control")
|
|
|
|
expect(moveToPreviousPhase(config, state)).toBe(true)
|
|
expect(state.phaseIndex).toBe(1)
|
|
})
|
|
|
|
it("finds the active clock for a timed phase and reports running state", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
const periodOne = config.phaseSequence[0]
|
|
const choiceOne = config.phaseSequence[1]
|
|
|
|
expect(activeClockForPhase(state, periodOne)).toBe(state.clocksByPhase.period_1)
|
|
expect(activeClockForPhase(state, choiceOne)).toBe(null)
|
|
expect(hasRunningClockOrTimer(state)).toBe(false)
|
|
|
|
state.clocksByPhase.period_1.running = true
|
|
expect(hasRunningClockOrTimer(state)).toBe(true)
|
|
})
|
|
|
|
it("stops all running auxiliary timers in place", () => {
|
|
const config = getMatchStateConfig("folkstyle_usa", "Bracket Round of 64")
|
|
const state = buildInitialState(config)
|
|
state.timers.w1.blood.running = true
|
|
state.timers.w1.blood.startedAt = 1_000
|
|
state.timers.w1.blood.remainingSeconds = 300
|
|
state.timers.w2.injury.running = true
|
|
state.timers.w2.injury.startedAt = 5_000
|
|
state.timers.w2.injury.remainingSeconds = 90
|
|
|
|
stopAllAuxiliaryTimers(state, 11_000)
|
|
|
|
expect(state.timers.w1.blood).toMatchObject({ running: false, startedAt: null, remainingSeconds: 290 })
|
|
expect(state.timers.w2.injury).toMatchObject({ running: false, startedAt: null, remainingSeconds: 84 })
|
|
})
|
|
})
|