parent
f96447c8d1
commit
f71ca0a52a
@ -0,0 +1,51 @@ |
||||
import { sequence } from "../../shared/array-utils.ts"; |
||||
import { BoardgameStateType, SquareState, formatSquareState } from "../../shared/datatypes.ts"; |
||||
|
||||
const getCellHtml = (key: string, state: BoardgameStateType, row: number, column: number) => { |
||||
if (!state.board) { |
||||
return ( |
||||
<button type="submit" name={key} value={state.serialize()} disabled> |
||||
{" "} |
||||
</button> |
||||
); |
||||
} |
||||
|
||||
const squareState = state.board.get(row, column); |
||||
const nextGameState = squareState === SquareState.Unoccupied ? state.withMove(row, column) : state; |
||||
return ( |
||||
<button |
||||
type="submit" |
||||
name={key} |
||||
value={nextGameState.serialize()} |
||||
disabled={squareState !== SquareState.Unoccupied} |
||||
> |
||||
{formatSquareState(squareState)} |
||||
</button> |
||||
); |
||||
}; |
||||
|
||||
export const getBoardgameHtml = (key: string, state: BoardgameStateType) => { |
||||
return ( |
||||
<board-game> |
||||
<form method="post"> |
||||
<p>Current player: {state.currentPlayerName}</p> |
||||
|
||||
<table> |
||||
<tbody> |
||||
{sequence(state.rows).map((row) => ( |
||||
<tr> |
||||
{sequence(state.columns).map((column) => ( |
||||
<td>{getCellHtml(key, state, row, column)}</td> |
||||
))} |
||||
</tr> |
||||
))} |
||||
</tbody> |
||||
</table> |
||||
|
||||
<button type="submit" name={key} value={state.withEmptyBoard().serialize()}> |
||||
Start game |
||||
</button> |
||||
</form> |
||||
</board-game> |
||||
); |
||||
}; |
@ -0,0 +1,19 @@ |
||||
import type { Request, Response } from "express"; |
||||
import { rewriteQueryParamsWith, safeGetQueryValue } from "../utils.ts"; |
||||
import { BoardgameState } from "../../shared/boardgame-state.ts"; |
||||
import { getBoardgameHtml } from "../components/boardgame.tsx"; |
||||
|
||||
// Returns nothing if query parameter is uninitialized and a redirect was made,
|
||||
// or component if query parameter is initialized.
|
||||
export const handleBoardgame = (req: Request, res: Response, key: string) => { |
||||
const serializedState = safeGetQueryValue(req, key); |
||||
const state = BoardgameState.fromSerialized(serializedState); |
||||
|
||||
if (!state) { |
||||
const newState = BoardgameState.createWithoutBoard(3, 3); |
||||
rewriteQueryParamsWith(req, res, { [key]: newState.serialize() }); |
||||
return; |
||||
} |
||||
|
||||
return getBoardgameHtml(key, state); |
||||
}; |
@ -1,11 +1,13 @@ |
||||
import express, { RequestHandler } from "express"; |
||||
import { rewriteQueryParams } from "./utils.ts"; |
||||
import { rewriteQueryParamsWith } from "./utils.ts"; |
||||
|
||||
export const createProgressiveForm = (mainHandler: RequestHandler) => { |
||||
const router = express.Router(); |
||||
|
||||
router.get("/", mainHandler); |
||||
router.post("/", rewriteQueryParams); |
||||
router.post("/", (req, res) => { |
||||
rewriteQueryParamsWith(req, res, (req.body || {}) as Record<string, unknown>); |
||||
}); |
||||
|
||||
return router; |
||||
}; |
||||
|
@ -0,0 +1,3 @@ |
||||
export const repeat = <T>(value: T, size: number) => [...(new Array(size) as unknown[])].map(() => value); |
||||
|
||||
export const sequence = (size: number) => [...(new Array(size) as unknown[])].map((_, index) => index); |
@ -0,0 +1,92 @@ |
||||
import { Board } from "./board.ts"; |
||||
import { |
||||
BoardType, |
||||
BoardgameStateType, |
||||
FIRST_PLAYER, |
||||
Player, |
||||
formatPlayer, |
||||
getNextPlayer, |
||||
getOccupiedStateByPlayer, |
||||
parsePlayer, |
||||
} from "./datatypes.ts"; |
||||
|
||||
const parseDimensions = (dimensionsSerialized: string | undefined) => { |
||||
if (!dimensionsSerialized) { |
||||
return null; |
||||
} |
||||
|
||||
const [rowsSerialized, columnsSerialized] = dimensionsSerialized.split("x"); |
||||
|
||||
if (!rowsSerialized || !columnsSerialized) { |
||||
// TODO: better error handling
|
||||
throw new Error("Incorrect dimensions"); |
||||
} |
||||
|
||||
return [parseInt(rowsSerialized, 10), parseInt(columnsSerialized, 10)] as const; |
||||
}; |
||||
|
||||
const parseBoard = (boardSerialized: string | undefined) => { |
||||
if (!boardSerialized) { |
||||
return null; |
||||
} |
||||
|
||||
return Board.fromSerialized(boardSerialized); |
||||
}; |
||||
|
||||
const serializeBoard = (board: BoardType | null) => { |
||||
if (!board) { |
||||
return ""; |
||||
} |
||||
|
||||
return board.serialize(); |
||||
}; |
||||
|
||||
export class BoardgameState implements BoardgameStateType { |
||||
constructor( |
||||
readonly rows: number, |
||||
readonly columns: number, |
||||
readonly currentPlayer: Player | null, |
||||
readonly board: BoardType | null, |
||||
) {} |
||||
|
||||
get currentPlayerName() { |
||||
return this.currentPlayer ? formatPlayer(this.currentPlayer) : ""; |
||||
} |
||||
|
||||
static createWithoutBoard(rows: number, columns: number) { |
||||
return new BoardgameState(rows, columns, null, null); |
||||
} |
||||
|
||||
withEmptyBoard() { |
||||
return new BoardgameState(this.rows, this.columns, FIRST_PLAYER, Board.createEmpty(this.rows, this.columns)); |
||||
} |
||||
|
||||
withMove(row: number, column: number) { |
||||
if (!this.currentPlayer || !this.board) { |
||||
throw new Error("Game is not started"); |
||||
} |
||||
|
||||
const nextBoard = this.board.with(row, column, getOccupiedStateByPlayer(this.currentPlayer)); |
||||
const nextPlayer = getNextPlayer(this.currentPlayer); |
||||
return new BoardgameState(this.rows, this.columns, nextPlayer, nextBoard); |
||||
} |
||||
|
||||
static fromSerialized(serialized: string | null) { |
||||
const [dimensionsSerialized, currentPlayerSerialized, boardSerialized] = (serialized ?? "").split("."); |
||||
|
||||
const dimensions = parseDimensions(dimensionsSerialized); |
||||
if (!dimensions) { |
||||
return null; |
||||
} |
||||
|
||||
const [rows, columns] = dimensions; |
||||
const currentPlayer = currentPlayerSerialized ? parsePlayer(currentPlayerSerialized) : null; |
||||
const board = parseBoard(boardSerialized); |
||||
|
||||
return new BoardgameState(rows, columns, currentPlayer, board); |
||||
} |
||||
|
||||
serialize() { |
||||
return `${this.rows}x${this.columns}.${this.currentPlayerName}.${serializeBoard(this.board)}`; |
||||
} |
||||
} |
@ -1,2 +1,5 @@ |
||||
// To simplify switches and provide some typecheck-time guarantees
|
||||
export const unreachable = (value: never) => value as unknown as string; |
||||
export const unreachableString = (value: never) => value as unknown as string; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- We don't care about the value here, but we do care that from TS point of view it has type `never`
|
||||
export const unreachableNull = (_value: never) => null; |
||||
|
Loading…
Reference in new issue