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 express, { RequestHandler } from "express"; |
||||||
import { rewriteQueryParams } from "./utils.ts"; |
import { rewriteQueryParamsWith } from "./utils.ts"; |
||||||
|
|
||||||
export const createProgressiveForm = (mainHandler: RequestHandler) => { |
export const createProgressiveForm = (mainHandler: RequestHandler) => { |
||||||
const router = express.Router(); |
const router = express.Router(); |
||||||
|
|
||||||
router.get("/", mainHandler); |
router.get("/", mainHandler); |
||||||
router.post("/", rewriteQueryParams); |
router.post("/", (req, res) => { |
||||||
|
rewriteQueryParamsWith(req, res, (req.body || {}) as Record<string, unknown>); |
||||||
|
}); |
||||||
|
|
||||||
return router; |
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
|
// 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