Last active
April 10, 2019 20:39
-
-
Save Spiralis/c442d6fcd34b42dfd535003bea7b2190 to your computer and use it in GitHub Desktop.
A simplified XState statecharts state-machine for running a handball game. Typically used with a control app for the match officials. Note: There are still parts missing in this sample, like disciplinary actions (2 min, yellow and red cards), as well as who made the goals, and adjusting the match-clock (adjusting the data on the fly). But, it is…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Available variables: | |
// Machine (machine factory function) | |
// XState (all XState exports) | |
function pad(n, width) { | |
var n = n + ""; | |
return n.length >= width ? n : new Array(width - n.length + 1).join("0") + n; | |
} | |
function displayTime(ticksInSecs) { | |
var ticks = ticksInSecs / 1000; | |
var hh = Math.floor(ticks / 3600); | |
var mm = Math.floor((ticks % 3600) / 60); | |
var ss = ticks % 60; | |
return pad(hh, 2) + ":" + pad(mm, 2) + ":" + pad(ss, 2); | |
} | |
const startClock = XState.actions.assign({ | |
start_ticks: (ctx, event) => Date.now() | |
}); | |
const stopClock = XState.actions.assign({ | |
match_ticks: (ctx, event) => ctx.match_ticks + Date.now() - ctx.start_ticks | |
}); | |
const setClockDisplay = XState.actions.assign({ | |
match_clock: (ctx, event) => displayTime(ctx.match_ticks) | |
}); | |
const incPeriod = XState.actions.assign({ | |
period: (ctx, event) => ctx.period + 1, | |
team: (ctx, event) => ({ | |
...ctx.team, | |
a: { | |
...ctx.team.a, | |
timeouts_in_period: 0 | |
}, | |
b: { | |
...ctx.team.b, | |
timeouts_in_period: 0 | |
} | |
}) | |
}); | |
const matchPlaying = (ctx, event) => { | |
return ctx.period > 0 && ctx.period <= ctx.setup.num_periods; | |
}; | |
const matchFinished = (ctx, event) => { | |
return ctx.period > ctx.setup.num_periods; | |
}; | |
const canTimeoutA = (ctx, event) => { | |
return ( | |
ctx.team.a.timeouts_in_period < ctx.setup.max_timeouts_per_period && | |
ctx.team.a.timeouts_total < ctx.setup.max_timeouts | |
); | |
}; | |
const canTimeoutB = (ctx, event) => { | |
return ( | |
ctx.team.b.timeouts_in_period < ctx.setup.max_timeouts_per_period && | |
ctx.team.b.timeouts_total < ctx.setup.max_timeouts | |
); | |
}; | |
const incTimeoutA = XState.actions.assign({ | |
team: (ctx, event) => ({ | |
...ctx.team, | |
a: { | |
...ctx.team.a, | |
timeouts_total: ctx.team.a.timeouts_total + 1, | |
timeouts_in_period: ctx.team.a.timeouts_in_period + 1 | |
} | |
}) | |
}); | |
const incTimeoutB = XState.actions.assign({ | |
team: (ctx, event) => ({ | |
...ctx.team, | |
b: { | |
...ctx.team.b, | |
timeouts_total: ctx.team.b.timeouts_total + 1, | |
timeouts_in_period: ctx.team.b.timeouts_in_period + 1 | |
} | |
}) | |
}); | |
const incGoalA = XState.actions.assign({ | |
team: (ctx, event) => ({ | |
...ctx.team, | |
a: { | |
...ctx.team.a, | |
goals: ctx.team.a.goals + 1 | |
} | |
}) | |
}); | |
const incGoalB = XState.actions.assign({ | |
team: (ctx, event) => ({ | |
...ctx.team, | |
b: { | |
...ctx.team.b, | |
goals: ctx.team.b.goals + 1 | |
} | |
}) | |
}); | |
const playingStates = { | |
intial: "running", | |
states: { | |
running: { | |
onEntry: "startClock", | |
onExit: ["stopClock", "setClockDisplay"], | |
on: { | |
STOP_TIME: "paused", | |
TIMEOUT_A: { | |
target: "in_timeout", | |
actions: "incTimeoutA", | |
cond: "canTimeoutA" | |
}, | |
TIMEOUT_B: { | |
target: "in_timeout", | |
actions: "incTimeoutB", | |
cond: "canTimeoutB" | |
}, | |
TIMER_PERIOD_BREAK: { | |
target: "in_period_break", | |
actions: "incPeriod" | |
} | |
} | |
}, | |
paused: { | |
on: { | |
START_TIME: "running" | |
} | |
}, | |
in_timeout: { | |
on: { | |
TIMER_TO_DONE: "running" | |
} | |
}, | |
in_period_break: { | |
on: { | |
"": [ | |
{ | |
target: "done", | |
cond: "matchFinished" | |
} | |
], | |
TIMER_PAUSE_DONE: "running" | |
} | |
}, | |
done: { | |
type: "final" | |
} | |
} | |
}; | |
const matchMachine = Machine( | |
{ | |
id: "match", | |
initial: "pending", | |
context: { | |
setup: { | |
num_periods: 2, | |
max_timeouts: 3, | |
max_timeouts_per_period: 2, | |
timeout_length: 60000 | |
}, | |
start_ticks: 0, // Time for last event, making it possible for stopClock to calculate what time to add on exit | |
match_ticks: 0, // Runtime for the match, increased on every stop of time in the match. | |
match_clock: "00:00:00", | |
period: 0, | |
team: { | |
a: { | |
timeouts_total: 0, | |
timeouts_in_period: 0, | |
goals: 0 | |
}, | |
b: { | |
timeouts_total: 0, | |
timeouts_in_period: 0, | |
goals: 0 | |
} | |
} | |
}, | |
states: { | |
pending: { | |
on: { | |
START: { | |
target: "playing", | |
actions: "incPeriod" | |
} | |
} | |
}, | |
playing: { | |
...playingStates, | |
onDone: { | |
target: "finished" | |
} | |
}, | |
finished: { | |
type: "final" | |
} | |
}, | |
on: { | |
HOME_GOAL: { | |
actions: "incGoalA", | |
cond: "matchPlaying" | |
}, | |
AWAY_GOAL: { | |
actions: "incGoalB", | |
cond: "matchPlaying" | |
} | |
} | |
}, | |
{ | |
actions: { | |
startClock, | |
stopClock, | |
setClockDisplay, | |
incPeriod, | |
incTimeoutA, | |
incTimeoutB, | |
incGoalA, | |
incGoalB | |
}, | |
guards: { matchPlaying, matchFinished, canTimeoutA, canTimeoutB } | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment