diff --git a/src/backend/components/boardgame.tsx b/src/backend/components/boardgame.tsx index ef237c3..232159e 100644 --- a/src/backend/components/boardgame.tsx +++ b/src/backend/components/boardgame.tsx @@ -61,12 +61,12 @@ export const getBoardgameHtml = (key: string, gameState: BoardgameStateType, rul const currentOutcome = gameState.board ? rules.getBoardOutcome(gameState.board) : CurrentOutcome.Undecided; return ( - -
+ +

Current player: {gameState.currentPlayerName}

- - +
+ {sequence(gameState.rows).map((row) => ( {sequence(gameState.columns).map((column) => ( diff --git a/src/frontend/components/board-game.ts b/src/frontend/components/board-game.ts new file mode 100644 index 0000000..1962eb0 --- /dev/null +++ b/src/frontend/components/board-game.ts @@ -0,0 +1,73 @@ +import { BoardgameState } from "../../shared/boardgame-state.ts"; +import { CurrentOutcome, SquareState, formatSquareState } from "../../shared/datatypes.ts"; +import { getDisplayStates } from "../../shared/display.ts"; +import { gamesRules } from "../../shared/rules.ts"; +import { TrackingTools } from "../lib/query-tracking-utils.ts"; + +export class BoardGameComponent extends HTMLElement { + private readonly trackingTools = new TrackingTools(this); + + handleTrackedValueUpdate(newValue: string | null) { + const rules = gamesRules.tictactoe; + + const gameState = BoardgameState.fromSerialized(newValue); + if (!gameState) { + throw new Error("Empty game state"); + } + const currentOutcome = gameState.board ? rules.getBoardOutcome(gameState.board) : CurrentOutcome.Undecided; + + const displayStates = getDisplayStates(gameState, currentOutcome); + for (const [className, shouldDisplay] of Object.entries(displayStates)) { + for (const element of this.querySelectorAll(`.${className}`)) { + (element as HTMLElement).style.display = shouldDisplay ? "" : "none"; + } + } + + for (const tbodyUntyped of this.querySelectorAll("tbody.game-board")) { + const tbody = tbodyUntyped as HTMLTableSectionElement; + for (let rowNumber = 0; rowNumber < tbody.rows.length; rowNumber++) { + const row = tbody.rows[rowNumber]; + if (!row) { + continue; + } + + for (let columnNumber = 0; columnNumber < row.cells.length; columnNumber++) { + const cell = row.cells[columnNumber]; + if (!cell) { + continue; + } + + for (const button of cell.querySelectorAll("button")) { + if (!gameState.board) { + button.value = gameState.serialize(); + button.disabled = true; + button.innerText = " "; + } else { + const squareState = gameState.board.get(rowNumber, columnNumber); + const nextGameState = + squareState === SquareState.Unoccupied && currentOutcome === CurrentOutcome.Undecided + ? gameState.withMove(rowNumber, columnNumber) + : gameState; + + button.value = nextGameState.serialize(); + button.disabled = nextGameState === gameState; + button.innerText = formatSquareState(squareState); + } + } + } + } + } + } + + connectedCallback() { + this.trackingTools.connectedCallback(); + } + + attributeChangedCallback() { + this.trackingTools.attributeChangedCallback(); + } + + disconnectedCallback() { + this.trackingTools.disconnectedCallback(); + } +} diff --git a/src/frontend/components/index.ts b/src/frontend/components/index.ts index 2d00c7f..a11166e 100644 --- a/src/frontend/components/index.ts +++ b/src/frontend/components/index.ts @@ -1,3 +1,4 @@ +import { BoardGameComponent } from "./board-game.ts"; import { ProgressiveForm } from "./progressive-form.ts"; import { ReactiveButton } from "./reactive-button.ts"; import { ReactiveSpan } from "./reactive-span.ts"; @@ -12,5 +13,7 @@ export const initializeWebComponents = () => { customElements.define("progressive-form", ProgressiveForm, { extends: "form" }); customElements.define("reactive-button", ReactiveButton, { extends: "button" }); customElements.define("reactive-span", ReactiveSpan, { extends: "span" }); + + customElements.define("board-game", BoardGameComponent); } };