dao-game / commit
Got local fully working.
src/game.ts | 4 +- src/local-game.ts | 414 ++++++++++++++++++++++++++++------------------------- src/remote-game.ts | 24 ++++ src/storage.ts | 18 +++ 4 files changed, 260 insertions(+), 200 deletions(-) diff --git a/src/game.ts b/src/game.ts index 5c5a68d..4b58f6d 100644 --- a/src/game.ts +++ b/src/game.ts @@ -7,7 +7,7 @@ export interface IGame { player2Id: string; currentPlayerId: string; canUndoMove: boolean; - getPlayerPositions: (isP1: boolean) => Coord[]; + getPlayerPositions: (playerId: string) => Coord[]; coordToBoardIndex: (coord: Coord) => number; getValidMoves: (start: Coord) => Coord[]; makeMove: (source: Coord, dest: Coord) => MoveResult; @@ -34,7 +34,7 @@ export class Game implements IGame { public get player1Id(): string { return this._player1Id; } public get player2Id(): string { return this._player2Id; } public get moveHistory(): Move[] { return this._moveHistory; } - public getPlayerPositions(isP1: boolean): Coord[] { return this.gameBoard.getCoords(isP1 ? this._player1Id : this.player2Id); } + public getPlayerPositions(playerId: string): Coord[] { return this.gameBoard.getCoords(playerId); } public coordToBoardIndex(coord: Coord): number { return this.gameBoard.coordToBoardIndex(coord); } public makeMove(source: Coord, dest: Coord): MoveResult { const checkForWin = (isP1Turn: boolean): MoveResult => { diff --git a/src/local-game.ts b/src/local-game.ts index 1b2dcda..a8d9ffa 100644 --- a/src/local-game.ts +++ b/src/local-game.ts @@ -9,222 +9,240 @@ const classNames = { possibleMove: "possible-move", }; -let selectedPiece: Element | null = null; -const game: IGame = new Game('Red', 'Blue'); -const listenerManager: ListenerManager = new ListenerManager(); - -// DOM references -const dom = { - defaultP1Piece: document.getElementById("default-p1-piece"), - defaultP2Piece: document.getElementById("default-p2-piece"), - resetButton: document.getElementById("reset-button") as HTMLInputElement, - undoButton: document.getElementById("undo-button") as HTMLInputElement, - cells: () => Array.from(document.querySelectorAll("td")), - allPieces: () => Array.from(document.querySelectorAll(".piece")), - p1TurnText: () => Array.from(document.querySelectorAll(".p1-turn-text")), - p2TurnText: () => Array.from(document.querySelectorAll(".p2-turn-text")), -}; - -const resetBoard = () => { - // reset Cells - dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); - // reset pieces - dom.allPieces().forEach(piece => { - piece.classList.remove(classNames.activePlayerPiece) - piece.classList.remove(classNames.selectedPiece) - }); - // remove all listeners - listenerManager.removeListeners(); -}; +export class LocalGame extends Game { + selectedPiece: Element | null = null; + listenerManager: ListenerManager = new ListenerManager(); + + // DOM references + private readonly dom = { + defaultP1Piece: document.getElementById("default-p1-piece"), + defaultP2Piece: document.getElementById("default-p2-piece"), + resetButton: document.getElementById("reset-button") as HTMLInputElement, + undoButton: document.getElementById("undo-button") as HTMLInputElement, + cells: () => Array.from(document.querySelectorAll("td")), + allPieces: () => Array.from(document.querySelectorAll(".piece")), + p1TurnText: () => Array.from(document.querySelectorAll(".p1-turn-text")), + p2TurnText: () => Array.from(document.querySelectorAll(".p2-turn-text")), + } -/** - * Animates the motion of piece moving from source to dest - * @param source Starting Coord - * @param dest Ending Coord - * @param playerId Player id - */ -const movePiece = async (source: Coord, dest: Coord) => { - const sourceIndex = game.coordToBoardIndex(source); - const destIndex = game.coordToBoardIndex(dest); - - // find current piece - const piece = dom.cells()[sourceIndex].children[0]; - // unselect piece - piece.classList.remove(classNames.selectedPiece); - - // create clone for new position - const destPiece = piece.cloneNode(true); - // add to destination cell - dom.cells()[destIndex].appendChild(destPiece); - - // start animations - const destAnimation = dom.cells()[destIndex].animate( - [{ opacity: 0 }, { opacity: 1 }], - { duration: 1000, iterations: 1, }); - const sourceAnimation = piece.animate( - [{ - opacity: 1, - width: "45px", - height: "45px", - }, { - opacity: 0, - width: "5px", - height: "5px", - }], - { duration: 1000, iterations: 1 } - ); - - // await animations - await Promise.all([sourceAnimation.finished, destAnimation.finished]); - - // remove old piece - piece.parentNode.removeChild(piece); -}; + private resetBoard() { + const { dom, listenerManager } = this; + // reset Cells + dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); + // reset pieces + dom.allPieces().forEach(piece => { + piece.classList.remove(classNames.activePlayerPiece) + piece.classList.remove(classNames.selectedPiece) + }); + // remove all listeners + listenerManager.removeListeners(); + } -const nextMove = () => { - const setActivePlayerPieces = () => { - const selectPiece = (piece: Element, source: Coord) => { - // clear current selection - if (selectedPiece !== null) { - selectedPiece.classList.remove(classNames.selectedPiece); - dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); - } + /** + * Animates the motion of piece moving from source to dest + * @param source Starting Coord + * @param dest Ending Coord + * @param playerId Player id + */ + private async movePiece(source: Coord, dest: Coord) { + const { dom } = this; + const sourceIndex = this.coordToBoardIndex(source); + const destIndex = this.coordToBoardIndex(dest); + + // find current piece + const piece = dom.cells()[sourceIndex].children[0]; + // unselect piece + piece.classList.remove(classNames.selectedPiece); + + // create clone for new position + const destPiece = piece.cloneNode(true); + // add to destination cell + dom.cells()[destIndex].appendChild(destPiece); + + // start animations + const destAnimation = dom.cells()[destIndex].animate( + [{ opacity: 0 }, { opacity: 1 }], + { duration: 1000, iterations: 1, }); + const sourceAnimation = piece.animate( + [{ + opacity: 1, + width: "45px", + height: "45px", + }, { + opacity: 0, + width: "5px", + height: "5px", + }], + { duration: 1000, iterations: 1 } + ); + + // await animations + await Promise.all([sourceAnimation.finished, destAnimation.finished]); + + // remove old piece + piece.parentNode.removeChild(piece); + } - // set as selected - piece.classList.add(classNames.selectedPiece); - selectedPiece = piece; - - // hook up allowed moves for: - // 1. blink - // 2. complete the move - const allowedMoves = game.getValidMoves(source); - allowedMoves.map((dest: Coord) => { - const cell = dom.cells()[game.coordToBoardIndex(dest)]; - cell.classList.add(classNames.possibleMove); - listenerManager.addListener(cell, "click", async () => await makeMove(source, dest)); + public nextMove() { + const setActivePlayerPieces = () => { + const { + dom, + listenerManager, + selectedPiece, + currentPlayerId, player1Id, player2Id, + } = this; + const selectPiece = (piece: Element, source: Coord) => { + // clear current selection + if (selectedPiece !== null) { + selectedPiece.classList.remove(classNames.selectedPiece); + dom.cells().forEach(cell => cell.classList.remove(classNames.possibleMove)); + } + + // set as selected + piece.classList.add(classNames.selectedPiece); + this.selectedPiece = piece; + + // hook up allowed moves for: + // 1. blink + // 2. complete the move + const allowedMoves = this.getValidMoves(source); + allowedMoves.map((dest: Coord) => { + const cell = dom.cells()[this.coordToBoardIndex(dest)]; + cell.classList.add(classNames.possibleMove); + listenerManager.addListener(cell, "click", async () => await finishMove(source, dest)); + }); + }; + + // complete the move from source to dest + const finishMove = async (source: Coord, dest: Coord) => { + listenerManager.removeListeners(); + + const result = this.makeMove(source, dest); + await this.movePiece(source, dest); + + if (result.isTerminal) { + alert(result.message); + document.location.reload(); + } else { + this.nextMove(); + } + }; + + // mark active pieces + const allCells = dom.cells(); + const playerPositions = this.getPlayerPositions(currentPlayerId); + const piecesAndPositions = playerPositions.map(coord => { + const index = this.coordToBoardIndex(coord); + const cell = allCells[index]; + const piece = cell.children[0]; + return { coord, piece }; }); + piecesAndPositions + .forEach(({ coord, piece }) => { + piece.classList.add(classNames.activePlayerPiece); + listenerManager.addListener(piece, "click", event => selectPiece(piece, coord)); + }); }; - // complete the move from source to dest - const makeMove = async (source: Coord, dest: Coord) => { - listenerManager.removeListeners(); - - const result = game.makeMove(source, dest); - await movePiece(source, dest); + const { + dom, + currentPlayerId, player1Id, player2Id, + canUndoMove, moveHistory, + } = this; + + // reset text + dom.p1TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); + dom.p2TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); + + // changePlayer + // 1. flip flag + // 2. set text + // 3. set click callbacks + const isP1Turn = currentPlayerId === player1Id; + if (isP1Turn) { + dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); + } else { + dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); + } - if (result.isTerminal) { - alert(result.message); - document.location.reload(); - } else { - nextMove(); - } - }; + dom.undoButton.disabled = canUndoMove; + if (moveHistory.length === 0) { + dom.undoButton.disabled = true; + dom.resetButton.disabled = true; + } else { + dom.undoButton.disabled = false; + dom.resetButton.disabled = false; + } - // mark active pieces - const allCells = dom.cells(); - const playerPositions = game.getPlayerPositions(game.currentPlayerId === game.player1Id); - const piecesAndPositions = playerPositions.map(coord => { - const index = game.coordToBoardIndex(coord); - const cell = allCells[index]; - const piece = cell.children[0]; - return { coord, piece }; - }); - piecesAndPositions - .forEach(({ coord, piece }) => { - piece.classList.add(classNames.activePlayerPiece); - listenerManager.addListener(piece, "click", event => selectPiece(piece, coord)); - }); - }; - - // reset text - dom.p1TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); - dom.p2TurnText().forEach((t) => t.classList.remove(classNames.selectedPlayerText)); - - // changePlayer - // 1. flip flag - // 2. set text - // 3. set click callbacks - const isP1Turn = game.currentPlayerId === game.player1Id; - if (isP1Turn) { - dom.p1TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); - } else { - dom.p2TurnText().forEach((t) => t.classList.add(classNames.selectedPlayerText)); - } + this.selectedPiece = null; - dom.undoButton.disabled = game.canUndoMove; - if (game.moveHistory.length === 0) { - dom.undoButton.disabled = true; - dom.resetButton.disabled = true; - } else { - dom.undoButton.disabled = false; - dom.resetButton.disabled = false; + this.resetBoard(); + setActivePlayerPieces(); } - selectedPiece = null; - - resetBoard(); - setActivePlayerPieces(); -}; - -const init = () => { - // create board and pieces - let isWhite = true; - const table = document.getElementById("table"); - for (let r = 0; r < game.size.row; r++) { - const tr = document.createElement("tr"); - table.appendChild(tr); - for (let c = 0; c < game.size.col; c++) { - const td = document.createElement("td"); - tr.appendChild(td); - td.className = isWhite ? "white" : "black"; + public init() { + const { dom, player1Id, player2Id, size } = this; + + // create board and pieces + let isWhite = true; + const table = document.getElementById("table"); + for (let r = 0; r < size.row; r++) { + const tr = document.createElement("tr"); + table.appendChild(tr); + for (let c = 0; c < size.col; c++) { + const td = document.createElement("td"); + tr.appendChild(td); + td.className = isWhite ? "white" : "black"; + isWhite = !isWhite; + } isWhite = !isWhite; } - isWhite = !isWhite; - } - const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => { - const defaultPiece = isP1Turn - ? dom.defaultP1Piece - : dom.defaultP2Piece; - const newPiece = defaultPiece.cloneNode( - true - ) as HTMLParagraphElement; - newPiece.id = pieceId; - const index = game.coordToBoardIndex(coord); - dom.cells()[index].appendChild(newPiece); - }; - game.getPlayerPositions(true).forEach(coord => createPiece(true, game.player1Id, coord)); - game.getPlayerPositions(false).forEach(coord => createPiece(false, game.player2Id, coord)); - - // hook up buttons - let blockActions = false; - dom.resetButton.addEventListener("click", () => { - if (blockActions) { - return; - } + const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => { + const defaultPiece = isP1Turn + ? dom.defaultP1Piece + : dom.defaultP2Piece; + const newPiece = defaultPiece.cloneNode( + true + ) as HTMLParagraphElement; + newPiece.id = pieceId; + const index = this.coordToBoardIndex(coord); + dom.cells()[index].appendChild(newPiece); + }; + this.getPlayerPositions(player1Id).forEach(coord => createPiece(true, player1Id, coord)); + this.getPlayerPositions(player2Id).forEach(coord => createPiece(false, player2Id, coord)); + + // hook up buttons + let blockActions = false; + dom.resetButton.addEventListener("click", () => { + if (blockActions) { + return; + } - blockActions = true; - if (confirm("Reset?")) { - window.location.reload(); - } - blockActions = false; - }); - dom.undoButton.addEventListener("click", async () => { - if (blockActions) { - return; - } + blockActions = true; + if (confirm("Reset?")) { + window.location.reload(); + } + blockActions = false; + }); + dom.undoButton.addEventListener("click", async () => { + if (blockActions) { + return; + } - blockActions = true; + blockActions = true; - const move = game.undoMove(); - await movePiece(move.dest, move.source); - nextMove(); + const move = this.undoMove(); + await this.movePiece(move.dest, move.source); + this.nextMove(); - blockActions = false; - }); -}; + blockActions = false; + }); + } +} // start! export const playGame = () => { - init(); - nextMove(); + const lg = new LocalGame('Red', 'Blue'); + lg.init(); + lg.nextMove(); }; diff --git a/src/remote-game.ts b/src/remote-game.ts new file mode 100644 index 0000000..4dd362f --- /dev/null +++ b/src/remote-game.ts @@ -0,0 +1,24 @@ +import Peer from 'peerjs' +import { LocalGame } from './local-game'; +import { useLocalStorage } from './storage'; + +interface LocalData { + selfPeerId: string; + otherPeerIds: string[] +} +const { getter: getData, setter: setData } = useLocalStorage<LocalData>('dao-game', + { selfPeerId: `dao-game-${prompt('Name?', 'Red') ?? 'Red'}`, otherPeerIds: ['dao-game-Blue'] }); + +export class PeerGame extends LocalGame { + public constructor() { + super('Local', 'Remote'); + + const { selfPeerId, otherPeerIds } = getData(); + const peer = new Peer(selfPeerId); + if (otherPeerIds.length > 0) { + const otherPeer = otherPeerIds[0]; + peer.connect(otherPeer); + } + + } +} \ No newline at end of file diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..c45bea2 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,18 @@ +export interface StorageInterface<T> { + getter: () => T, + setter: (data: T) => void +} +export const useLocalStorage = <T>(key: string, defaultData?: T): StorageInterface<T> => { + return { + getter: () => { + let item = localStorage.getItem(key); + if (item === null && defaultData !== undefined) { + item = JSON.stringify(defaultData); + localStorage.setItem(key, item); + } + const data = JSON.parse(item) as T; + return data; + }, + setter: (data: T) => localStorage.setItem(key, JSON.stringify(data as T)) + }; +} \ No newline at end of file