Created
September 19, 2022 05:13
-
-
Save zwade/93f114404d3f9dbb713190d4735f8be1 to your computer and use it in GitHub Desktop.
A sample FSM description in TypeScript
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
// StateMachine Types and Helpers | |
const opaquenessProof = Symbol.for("Opaqueness Proof"); | |
type Args<T> = T extends (...args: infer Args) => any ? Args : never; | |
type OpaqueResult = { | |
[opaquenessProof]: true; | |
nextState: string; | |
args: any[]; | |
}; | |
type StateMachine = Record<string, (...args: any[]) => OpaqueResult | undefined> & { | |
start: () => OpaqueResult | undefined; | |
}; | |
const next = <T, K extends keyof T>(_this: T, nextState: K, ...args: Args<T[K]>): OpaqueResult => { | |
return { | |
[opaquenessProof]: true, | |
nextState: nextState as string, | |
args, | |
}; | |
}; | |
// TicTacToe helpers | |
type Board = (string | null)[][]; | |
const emptyBoard = () => [ | |
[null, null, null], | |
[null, null, null], | |
[null, null, null], | |
]; | |
const makeMove = (board: Board, player: string, move: string | null) => { | |
if (!move) { | |
throw new Error("Must specify a move"); | |
} | |
const asCoords = move.split(/,\s*/); | |
if (asCoords.length !== 2) { | |
throw new Error("Specify move as `x, y`"); | |
} | |
const x = Number(asCoords[0]); | |
const y = Number(asCoords[1]); | |
if (board[y][x] !== null) { | |
throw Error("Invalid Move"); | |
} | |
const newBoard = board.map((row) => row.slice()); | |
newBoard[y][x] = player; | |
return newBoard; | |
}; | |
const hasWon = (player: string, board: Board) => { | |
const checkRow = (row: (string | null)[]) => row.every((cell) => cell === player); | |
const checkCol = (col: number) => board.every((row) => row[col] === player); | |
const checkDiag = (row: number, col: number, dir: 1 | -1) => { | |
return board.every((_, i) => board[i][col + (i - row) * dir] === player); | |
}; | |
return ( | |
checkRow(board[0]) || | |
checkRow(board[1]) || | |
checkRow(board[2]) || | |
checkCol(0) || | |
checkCol(1) || | |
checkCol(2) || | |
checkDiag(0, 0, 1) || | |
checkDiag(0, 2, -1) | |
); | |
}; | |
// StateMachine executor | |
const execute = <T extends StateMachine>( | |
stateMachine: T, | |
startState: { kind: string; args: any[] } = { kind: "start", args: [] }, | |
): void => { | |
const { kind, args } = startState; | |
const nextState = stateMachine[kind]; | |
const result = nextState(...args); | |
if (result === undefined) { | |
return; | |
} | |
execute(stateMachine, { kind: result.nextState, args: result.args }); | |
}; | |
// StateMachine definition | |
const games = { | |
start() { | |
const game = prompt("What would you like to play?"); | |
if (game === "tic-tac-toe") { | |
return next(this, "ticTacToe"); | |
} | |
return undefined; | |
}, | |
ticTacToe() { | |
return next(this, "turn", "O", emptyBoard()); | |
}, | |
turn(player: string, board: Board): OpaqueResult { | |
const move = prompt(`Which move will ${player} make?`); | |
const newBoard = makeMove(board, player, move); | |
if (hasWon(player, newBoard)) { | |
return next(this, "win", player); | |
} else { | |
const nextPlayer = player === "O" ? "X" : "O"; | |
return next(this, "turn", nextPlayer, newBoard); | |
} | |
}, | |
win(player: string) { | |
alert(`${player} Won!`); | |
return undefined; | |
}, | |
}; | |
const _GAMES_IS_A_STATEMACHINE: typeof games extends StateMachine ? true : false = true; | |
// Run the game | |
execute(games); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment