Skip to content

Instantly share code, notes, and snippets.

@zwade
Created September 19, 2022 05:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zwade/93f114404d3f9dbb713190d4735f8be1 to your computer and use it in GitHub Desktop.
Save zwade/93f114404d3f9dbb713190d4735f8be1 to your computer and use it in GitHub Desktop.
A sample FSM description in TypeScript
// 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