dao-game / commit
In-progress code, doesn't compile, etc.
.vscode/settings.json | 1 + src/app.ts | 0 src/game.ts | 558 ++++++++++++-------------------------------------- src/local-game.ts | 469 ++++++++++++++++++++++++++++++++++++++++++ src/peering_.ts | 119 +++++++++++ src/piano.ts | 53 ++--- src/types.ts | 99 +++++---- 7 files changed, 801 insertions(+), 498 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e2d4fa..03ad85b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "esbuild", + "Newp", "outfile", "servedir", "Todos" diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/game.ts b/src/game.ts index 19371a7..b4233ca 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,444 +1,152 @@ -import { PeerComm } from "./peering"; -import { Coord, GameBoard, ListenerManager, Move } from "./types"; - -const constants = { - selectedPieceClassName: "selected-piece", - selectedPlayerTextClassName: "selected-player-text", - activePlayerPieceClassName: "active-player-piece", - possibleMoveClassName: "possible-move", -}; - -const gameBoard = new GameBoard(); -const listenerManager: ListenerManager = new ListenerManager(); - -const player: { - isPlayerRed: boolean; - isRedTurn: boolean; - selectedPieceId: number | undefined; - moveHistory: Move[]; -} = { - isPlayerRed: false, - isRedTurn: false, - selectedPieceId: undefined, - moveHistory: [], -}; - -// DOM references -const dom = { - defaultRedPiece: document.getElementById("default-red-piece"), - defaultBluePiece: document.getElementById("default-blue-piece"), - cells: () => Array.from(document.querySelectorAll("td")), - allPieces: () => Array.from(document.querySelectorAll(".piece")), - playerPieces: (isRedTurn: boolean) => - isRedTurn - ? Array.from( - document.querySelectorAll(".red-piece:not(#default-red-piece") - ) - : Array.from( - document.querySelectorAll( - ".blue-piece:not(#default-blue-piece" - ) - ), - redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")), - blueTurnText: () => - Array.from(document.querySelectorAll(".blue-turn-text")), - resetButton: document.getElementById("reset-button") as HTMLInputElement, - undoButton: document.getElementById("undo-button") as HTMLInputElement, -}; - -const resetBoard = () => { - // resetCells - dom.cells().forEach((cell) => { - cell.classList.remove(constants.possibleMoveClassName); - }); - - // reset pieces - dom.allPieces().forEach((piece) => { - if (piece.classList.contains(constants.activePlayerPieceClassName)) { - piece.classList.toggle(constants.activePlayerPieceClassName); - } - if (piece.classList.contains(constants.selectedPieceClassName)) { - piece.classList.toggle(constants.selectedPieceClassName); - } - }); - - // remove all listeners - listenerManager.removeListeners(); -}; - -const movePiece = async (sourceIndex: number, destIndex: number) => { - const pieceId = gameBoard.board[sourceIndex]; - // find current piece - const piece = document.getElementById(pieceId.toString()); - // unselect piece - piece.classList.remove(constants.selectedPieceClassName); - - // 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); - gameBoard.board[sourceIndex] = null; - // set new piece - gameBoard.board[destIndex] = pieceId; -}; - -const nextMove = () => { - const setActivePlayerPieces = () => { - const selectPiece = (event: Event) => { - const getAllowedMoveIndicesForIndex = ( - boardIndex: number - ): number[] => { - const getFarthestCoord = ( - startCoord: Coord, - direction: Coord - ): Coord => { - let farthestPos = undefined; - let testPos = startCoord; - while (true) { - const pos = new Coord( - testPos.row + direction.row, - testPos.col + direction.col - ); - const index = gameBoard.coordToBoardIndex(pos); - const withinBounds = - pos.row >= 0 && - pos.row < gameBoard.rows && - pos.col >= 0 && - pos.col < gameBoard.cols; - const isOccupied = Number.isInteger( - gameBoard.board[index] - ); - if (isOccupied || !withinBounds) { - break; - } else { - farthestPos = pos; - testPos = pos; - } - } - return farthestPos; - }; - - const coord = gameBoard.boardIndexToCoord(boardIndex); - - const farthestCoords = Coord.allDirections - .map((direction) => getFarthestCoord(coord, direction)) - .filter((farCoord) => farCoord !== undefined); +import { Coord, GameBoard, IGame, Move, MoveResult } from "./types"; + +export class Game implements IGame { + private readonly gameBoard: GameBoard; + private readonly player1Id: string; + private readonly player2Id: string; + private isGameOver: boolean; + private readonly _moveHistory: Move[]; + + public constructor(player1Id: string, player2Id: string) { + this.gameBoard = new GameBoard(player1Id, player2Id); + this.player1Id = player1Id; + this.player2Id = player2Id; + this.isGameOver = false; + this._moveHistory = []; + } - const farthestIndices = farthestCoords.map((c) => - gameBoard.coordToBoardIndex(c) - ); - return farthestIndices; + public get moveHistory(): Move[] { return this._moveHistory; } + public currentPlayerId: string; + public makeMove(source: Coord, dest: Coord): MoveResult { + const checkForWin = (isP1Turn: boolean): MoveResult => { + const doCoordsFormSquare = (coords: Coord[]): boolean => { + const allRows = coords.map((c) => c.row); + const allCols = coords.map((c) => c.col); + const min = new Coord(Math.min(...allRows), Math.min(...allCols)); + const max = new Coord(Math.max(...allRows), Math.max(...allCols)); + const diff = max.sub(min); + const isSquare = diff.row === 1 && diff.col === 1; + return isSquare; }; - - const previousSelectedPieceId = player.selectedPieceId; - if (previousSelectedPieceId !== undefined) { - const previousPiece = document.getElementById( - previousSelectedPieceId.toString() - ); - previousPiece.classList.remove( - constants.selectedPieceClassName - ); - dom.cells().forEach((cell) => { - cell.classList.remove(constants.possibleMoveClassName); - }); + const { gameBoard, player1Id, player2Id } = this; + const playerId = isP1Turn ? player1Id : player2Id; + + const playerCoords = gameBoard.getCoords(playerId) + const squareCoords: Coord[] = [ + new Coord(0, 0), + new Coord(3, 0), + new Coord(3, 3), + new Coord(0, 3), + ]; + + // winning conditions: + // 1. all in one row + // 2. all in one column + // 3. all in corners + // 4. all in square + // losing conditions: + // 1. on three sides of an opponent + + const firstPiece = playerCoords[0]; + const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row); + const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col); + const allInCorners = playerCoords.every( + (pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0 + ); + const allInSquare = doCoordsFormSquare(playerCoords); + + const winnerName = isP1Turn ? "Player 1" : "Player 2"; + let winningMessage = undefined; + if (allInRow) { + winningMessage = `${winnerName} wins by forming a row`; + } + if (allInCol) { + winningMessage = `${winnerName} wins by forming a column`; + } + if (allInCorners) { + winningMessage = `${winnerName} wins by putting pieces in corners`; + } + if (allInSquare) { + winningMessage = `${winnerName} wins by forming a square`; + } + if (winningMessage) { + return { + isTerminal: true, + message: winningMessage + }; + } else { + return { + isTerminal: false + }; } - // read event data - const piece = event.target as Element; - const pieceId = parseInt(piece.id); - - // set as selected - player.selectedPieceId = pieceId; - piece.classList.add(constants.selectedPieceClassName); - - // hook up allowed moves for: - // 1. blink - // 2. complete the move - const pieceIndex = gameBoard.board.indexOf(pieceId); - const allowedMoveIndices = - getAllowedMoveIndicesForIndex(pieceIndex); - allowedMoveIndices.forEach((i) => { - const cell = dom.cells()[i]; - listenerManager.addListener( - cell, - "click", - async () => await makeMove(i) - ); - cell.classList.add(constants.possibleMoveClassName); - }); - }; - - const makeMove = async (destIndex) => { - listenerManager.removeListeners(); - - const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId); - player.moveHistory.push(new Move(sourceIndex, destIndex)); - - await movePiece(sourceIndex, destIndex); - - checkForWin(); - nextMove(); }; - // mark active pieces - const playerPieces = dom.playerPieces(player.isRedTurn); - playerPieces.forEach((pp) => { - pp.classList.add(constants.activePlayerPieceClassName); - listenerManager.addListener(pp, "click", selectPiece); - }); - }; + const { gameBoard, player1Id, player2Id } = this; + const isP1Turn = (this.currentPlayerId === player1Id); + const playerId = isP1Turn ? player1Id : player2Id; - // reset text - dom.redTurnText().forEach((t) => - t.classList.remove(constants.selectedPlayerTextClassName) - ); - dom.blueTurnText().forEach((t) => - t.classList.remove(constants.selectedPlayerTextClassName) - ); + gameBoard.move(source, dest); - // changePlayer - // 1. flip flag - // 2. set text - // 3. set click callbacks - if (player.isRedTurn) { - player.isRedTurn = false; - dom.blueTurnText().forEach((t) => - t.classList.add(constants.selectedPlayerTextClassName) - ); - } else { - player.isRedTurn = true; - dom.redTurnText().forEach((t) => - t.classList.add(constants.selectedPlayerTextClassName) - ); - } - - if (player.moveHistory.length === 0) { - dom.undoButton.disabled = true; - dom.resetButton.disabled = true; - } else { - dom.undoButton.disabled = false; - dom.resetButton.disabled = false; + const result = checkForWin(isP1Turn); + if (!result.isTerminal) { + this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id; + this.isGameOver = true; + } else { + this._moveHistory.push(new Move(playerId, source, dest)); + } + return result; } - - player.selectedPieceId = undefined; - resetBoard(); - setActivePlayerPieces(); -}; - -const checkForWin = () => { - const elementsToCoords = (elements: Element[]): Coord[] => { - const elementsToBoardIndexes = (elements: Element[]): number[] => { - const indices = elements.map((pp) => { - const pieceId = Number.parseInt(pp.id); - const pieceIndex = gameBoard.board.indexOf(pieceId); - return pieceIndex; - }); - return indices; + public getValidMoves(start: Coord): Coord[] { + const getFarthestCoord = ( + startCoord: Coord, + direction: Coord + ): Coord => { + const { gameBoard } = this; + let farthestPos = undefined; + let testPos = startCoord; + while (true) { + const pos = new Coord( + testPos.row + direction.row, + testPos.col + direction.col + ); + const withinBounds = + pos.row >= 0 && + pos.row < gameBoard.rows && + pos.col >= 0 && + pos.col < gameBoard.cols; + + const isOccupied = gameBoard.get(pos) !== null; + if (isOccupied || !withinBounds) { + break; + } else { + farthestPos = pos; + testPos = pos; + } + } + return farthestPos; }; - const indices = elementsToBoardIndexes(elements); - const coords = indices.map((pieceIndex) => - gameBoard.boardIndexToCoord(pieceIndex) - ); - return coords; - }; - const doCoordsFormSquare = (coords: Coord[]): boolean => { - const allRows = coords.map((c) => c.row); - const allCols = coords.map((c) => c.col); - const min = new Coord(Math.min(...allRows), Math.min(...allCols)); - const max = new Coord(Math.max(...allRows), Math.max(...allCols)); - const diff = max.sub(min); - const isSquare = diff.row === 1 && diff.col === 1; - return isSquare; - }; - - const playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn)); - const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn)); - const squareCoords: Coord[] = [ - new Coord(0, 0), - new Coord(3, 0), - new Coord(3, 3), - new Coord(0, 3), - ]; - - // winning conditions: - // 1. all in one row - // 2. all in one column - // 3. all in corners - // 4. all in square - // losing conditions: - // 1. on three sides of an opponent + const farthestCoords = Coord.allDirections + .map((direction) => getFarthestCoord(start, direction)) + .filter((farCoord) => farCoord !== undefined); - const firstPiece = playerCoords[0]; - const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row); - const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col); - const allInCorners = playerCoords.every( - (pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0 - ); - const allInSquare = doCoordsFormSquare(playerCoords); - - const winnerName = player.isRedTurn ? "Red" : "Blue"; - let winningMessage = undefined; - if (allInRow) { - winningMessage = `${winnerName} wins by forming a row`; - } - if (allInCol) { - winningMessage = `${winnerName} wins by forming a column`; - } - if (allInCorners) { - winningMessage = `${winnerName} wins by putting pieces in corners`; + return farthestCoords; } - if (allInSquare) { - winningMessage = `${winnerName} wins by forming a square`; - } - - if (winningMessage !== undefined) { - alert(winningMessage); - document.location.reload(); - } -}; - -const init = () => { - // create board and pieces - let isWhite = true; - const table = document.getElementById("table"); - for (let r = 0; r < gameBoard.rows; r++) { - const tr = document.createElement("tr"); - table.appendChild(tr); - for (let c = 0; c < gameBoard.cols; c++) { - const td = document.createElement("td"); - tr.appendChild(td); - td.className = isWhite ? "white" : "black"; - isWhite = !isWhite; + public undoMove(): void { + if (!this.canUndoMove) { + throw "Newp! Can't undo!" } - isWhite = !isWhite; + const { gameBoard } = this; + const { source, dest, playerId } = this._moveHistory.pop(); + this.currentPlayerId = playerId; + gameBoard.move(dest, source); } - gameBoard.board.forEach((value, index) => { - if (value !== null) { - const isRed = value < 4; - const defaultPiece = isRed - ? dom.defaultRedPiece - : dom.defaultBluePiece; - const newPiece = defaultPiece.cloneNode( - true - ) as HTMLParagraphElement; - newPiece.id = value.toString(); - dom.cells()[index].appendChild(newPiece); - } - }); - - // hook up buttons - let blockActions = false; - dom.resetButton.addEventListener("click", () => { - if (blockActions) { - return; + public get canUndoMove(): boolean { + if (this._moveHistory.length === 0 || this.isGameOver) { + return false; } - if (player.moveHistory.length === 0) { - return; - } - - blockActions = true; - if (confirm("Reset?")) { - window.location.reload(); - } - blockActions = false; - }); - dom.undoButton.addEventListener("click", async () => { - if (blockActions) { - return; - } - if (player.moveHistory.length === 0) { - return; - } - - blockActions = true; - - player.selectedPieceId = undefined; - resetBoard(); - - const { destIndex, sourceIndex } = player.moveHistory.pop(); - await movePiece(destIndex, sourceIndex); - nextMove(); - blockActions = false; - }); -}; - -// start! -const playGame = () => { - init(); - nextMove(); -}; - -// const isOnlineGame = (window as any).daoIsOnline; -// if (isOnlineGame) { -// const receiver = (data) => -// const peer = new PeerComm() -// } else { -// playGame(); - -// } - -playGame(); - -function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} -const doStuff = async () => { - const peer = new PeerComm((data) => { - console.log(`TO: ${peer.id})`); - console.log(`FROM: ?)`); - console.log(`DATA: ${data}`); - }); - prompt('Self id:', peer.id); - const otherId = prompt('Peer id to connect to:', ''); - if (!otherId) { - while (!peer.isConnected) { - await delay(500); - } - } else { - peer.connect(otherId); + return true; } - alert('Connected!'); - alert('Starting messaging server'); - alert('Respond with empty text to stop'); - - while (true) { - const message = prompt('Message to send:', ''); - if (!message) { - break; - } - peer.send(message); - delay(1000); - } } -// doStuff(); diff --git a/src/local-game.ts b/src/local-game.ts new file mode 100644 index 0000000..bcc0870 --- /dev/null +++ b/src/local-game.ts @@ -0,0 +1,469 @@ +import { PeerComm } from "./peering"; +import { Coord, GameBoard, Move } from "./types"; + +const constants = { + selectedPieceClassName: "selected-piece", + selectedPlayerTextClassName: "selected-player-text", + activePlayerPieceClassName: "active-player-piece", + possibleMoveClassName: "possible-move", +}; + +interface Listener { + element: Element; + type: string; + listener: (event: Event) => void; +} + +export class ListenerManager { + private listeners: Listener[] = []; + + public addListener(element: Element, type: string, listener: (event: Event) => void): void { + const { listeners } = this; + element.addEventListener(type, listener); + listeners.push({ element, type, listener }); + } + + public removeListeners() { + const { listeners } = this; + listeners.forEach(value => { + const { element, type, listener } = value; + element.removeEventListener(type, listener); + }); + listeners.length = 0; + } +} + +const gameBoard = new GameBoard(); +const listenerManager: ListenerManager = new ListenerManager(); + +const player: { + isPlayerRed: boolean; + isRedTurn: boolean; + selectedPieceId: number | undefined; + moveHistory: Move[]; +} = { + isPlayerRed: false, + isRedTurn: false, + selectedPieceId: undefined, + moveHistory: [], +}; + +// DOM references +const dom = { + defaultRedPiece: document.getElementById("default-red-piece"), + defaultBluePiece: document.getElementById("default-blue-piece"), + cells: () => Array.from(document.querySelectorAll("td")), + allPieces: () => Array.from(document.querySelectorAll(".piece")), + playerPieces: (isRedTurn: boolean) => + isRedTurn + ? Array.from( + document.querySelectorAll(".red-piece:not(#default-red-piece") + ) + : Array.from( + document.querySelectorAll( + ".blue-piece:not(#default-blue-piece" + ) + ), + redTurnText: () => Array.from(document.querySelectorAll(".red-turn-text")), + blueTurnText: () => + Array.from(document.querySelectorAll(".blue-turn-text")), + resetButton: document.getElementById("reset-button") as HTMLInputElement, + undoButton: document.getElementById("undo-button") as HTMLInputElement, +}; + +const resetBoard = () => { + // resetCells + dom.cells().forEach((cell) => { + cell.classList.remove(constants.possibleMoveClassName); + }); + + // reset pieces + dom.allPieces().forEach((piece) => { + if (piece.classList.contains(constants.activePlayerPieceClassName)) { + piece.classList.toggle(constants.activePlayerPieceClassName); + } + if (piece.classList.contains(constants.selectedPieceClassName)) { + piece.classList.toggle(constants.selectedPieceClassName); + } + }); + + // remove all listeners + listenerManager.removeListeners(); +}; + +const movePiece = async (sourceIndex: number, destIndex: number) => { + const pieceId = gameBoard.board[sourceIndex]; + // find current piece + const piece = document.getElementById(pieceId.toString()); + // unselect piece + piece.classList.remove(constants.selectedPieceClassName); + + // 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); + gameBoard.board[sourceIndex] = null; + // set new piece + gameBoard.board[destIndex] = pieceId; +}; + +const nextMove = () => { + const setActivePlayerPieces = () => { + const selectPiece = (event: Event) => { + const getAllowedMoveIndicesForIndex = ( + boardIndex: number + ): number[] => { + const getFarthestCoord = ( + startCoord: Coord, + direction: Coord + ): Coord => { + let farthestPos = undefined; + let testPos = startCoord; + while (true) { + const pos = new Coord( + testPos.row + direction.row, + testPos.col + direction.col + ); + const index = gameBoard.coordToBoardIndex(pos); + const withinBounds = + pos.row >= 0 && + pos.row < gameBoard.rows && + pos.col >= 0 && + pos.col < gameBoard.cols; + const isOccupied = Number.isInteger( + gameBoard.board[index] + ); + if (isOccupied || !withinBounds) { + break; + } else { + farthestPos = pos; + testPos = pos; + } + } + return farthestPos; + }; + + const coord = gameBoard.boardIndexToCoord(boardIndex); + + const farthestCoords = Coord.allDirections + .map((direction) => getFarthestCoord(coord, direction)) + .filter((farCoord) => farCoord !== undefined); + + const farthestIndices = farthestCoords.map((c) => + gameBoard.coordToBoardIndex(c) + ); + return farthestIndices; + }; + + const previousSelectedPieceId = player.selectedPieceId; + if (previousSelectedPieceId !== undefined) { + const previousPiece = document.getElementById( + previousSelectedPieceId.toString() + ); + previousPiece.classList.remove( + constants.selectedPieceClassName + ); + dom.cells().forEach((cell) => { + cell.classList.remove(constants.possibleMoveClassName); + }); + } + + // read event data + const piece = event.target as Element; + const pieceId = parseInt(piece.id); + + // set as selected + player.selectedPieceId = pieceId; + piece.classList.add(constants.selectedPieceClassName); + + // hook up allowed moves for: + // 1. blink + // 2. complete the move + const pieceIndex = gameBoard.board.indexOf(pieceId); + const allowedMoveIndices = + getAllowedMoveIndicesForIndex(pieceIndex); + allowedMoveIndices.forEach((i) => { + const cell = dom.cells()[i]; + listenerManager.addListener( + cell, + "click", + async () => await makeMove(i) + ); + cell.classList.add(constants.possibleMoveClassName); + }); + }; + + const makeMove = async (destIndex) => { + listenerManager.removeListeners(); + + const sourceIndex = gameBoard.board.indexOf(player.selectedPieceId); + player.moveHistory.push(new Move(sourceIndex, destIndex)); + + await movePiece(sourceIndex, destIndex); + + checkForWin(); + nextMove(); + }; + + // mark active pieces + const playerPieces = dom.playerPieces(player.isRedTurn); + playerPieces.forEach((pp) => { + pp.classList.add(constants.activePlayerPieceClassName); + listenerManager.addListener(pp, "click", selectPiece); + }); + }; + + // reset text + dom.redTurnText().forEach((t) => + t.classList.remove(constants.selectedPlayerTextClassName) + ); + dom.blueTurnText().forEach((t) => + t.classList.remove(constants.selectedPlayerTextClassName) + ); + + // changePlayer + // 1. flip flag + // 2. set text + // 3. set click callbacks + if (player.isRedTurn) { + player.isRedTurn = false; + dom.blueTurnText().forEach((t) => + t.classList.add(constants.selectedPlayerTextClassName) + ); + } else { + player.isRedTurn = true; + dom.redTurnText().forEach((t) => + t.classList.add(constants.selectedPlayerTextClassName) + ); + } + + if (player.moveHistory.length === 0) { + dom.undoButton.disabled = true; + dom.resetButton.disabled = true; + } else { + dom.undoButton.disabled = false; + dom.resetButton.disabled = false; + } + + player.selectedPieceId = undefined; + resetBoard(); + setActivePlayerPieces(); +}; + +const checkForWin = () => { + const elementsToCoords = (elements: Element[]): Coord[] => { + const elementsToBoardIndexes = (elements: Element[]): number[] => { + const indices = elements.map((pp) => { + const pieceId = Number.parseInt(pp.id); + const pieceIndex = gameBoard.board.indexOf(pieceId); + return pieceIndex; + }); + return indices; + }; + + const indices = elementsToBoardIndexes(elements); + const coords = indices.map((pieceIndex) => + gameBoard.boardIndexToCoord(pieceIndex) + ); + return coords; + }; + const doCoordsFormSquare = (coords: Coord[]): boolean => { + const allRows = coords.map((c) => c.row); + const allCols = coords.map((c) => c.col); + const min = new Coord(Math.min(...allRows), Math.min(...allCols)); + const max = new Coord(Math.max(...allRows), Math.max(...allCols)); + const diff = max.sub(min); + const isSquare = diff.row === 1 && diff.col === 1; + return isSquare; + }; + + const playerCoords = elementsToCoords(dom.playerPieces(player.isRedTurn)); + const otherCoords = elementsToCoords(dom.playerPieces(!player.isRedTurn)); + const squareCoords: Coord[] = [ + new Coord(0, 0), + new Coord(3, 0), + new Coord(3, 3), + new Coord(0, 3), + ]; + + // winning conditions: + // 1. all in one row + // 2. all in one column + // 3. all in corners + // 4. all in square + // losing conditions: + // 1. on three sides of an opponent + + const firstPiece = playerCoords[0]; + const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row); + const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col); + const allInCorners = playerCoords.every( + (pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0 + ); + const allInSquare = doCoordsFormSquare(playerCoords); + + const winnerName = player.isRedTurn ? "Red" : "Blue"; + let winningMessage = undefined; + if (allInRow) { + winningMessage = `${winnerName} wins by forming a row`; + } + if (allInCol) { + winningMessage = `${winnerName} wins by forming a column`; + } + if (allInCorners) { + winningMessage = `${winnerName} wins by putting pieces in corners`; + } + if (allInSquare) { + winningMessage = `${winnerName} wins by forming a square`; + } + + if (winningMessage !== undefined) { + alert(winningMessage); + document.location.reload(); + } +}; + +const init = () => { + // create board and pieces + let isWhite = true; + const table = document.getElementById("table"); + for (let r = 0; r < gameBoard.rows; r++) { + const tr = document.createElement("tr"); + table.appendChild(tr); + for (let c = 0; c < gameBoard.cols; c++) { + const td = document.createElement("td"); + tr.appendChild(td); + td.className = isWhite ? "white" : "black"; + isWhite = !isWhite; + } + isWhite = !isWhite; + } + gameBoard.board.forEach((value, index) => { + if (value !== null) { + const isRed = value < 4; + const defaultPiece = isRed + ? dom.defaultRedPiece + : dom.defaultBluePiece; + const newPiece = defaultPiece.cloneNode( + true + ) as HTMLParagraphElement; + newPiece.id = value.toString(); + dom.cells()[index].appendChild(newPiece); + } + }); + + // hook up buttons + let blockActions = false; + dom.resetButton.addEventListener("click", () => { + if (blockActions) { + return; + } + if (player.moveHistory.length === 0) { + return; + } + + blockActions = true; + if (confirm("Reset?")) { + window.location.reload(); + } + blockActions = false; + }); + dom.undoButton.addEventListener("click", async () => { + if (blockActions) { + return; + } + if (player.moveHistory.length === 0) { + return; + } + + blockActions = true; + + player.selectedPieceId = undefined; + resetBoard(); + + const { destIndex, sourceIndex } = player.moveHistory.pop(); + await movePiece(destIndex, sourceIndex); + nextMove(); + blockActions = false; + }); +}; + +// start! +const playGame = () => { + init(); + nextMove(); +}; + +// const isOnlineGame = (window as any).daoIsOnline; +// if (isOnlineGame) { +// const receiver = (data) => +// const peer = new PeerComm() +// } else { +// playGame(); + +// } + +playGame(); + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +const doStuff = async () => { + const peer = new PeerComm((data) => { + console.log(`TO: ${peer.id})`); + console.log(`FROM: ?)`); + console.log(`DATA: ${data}`); + }); + prompt('Self id:', peer.id); + const otherId = prompt('Peer id to connect to:', ''); + if (!otherId) { + while (!peer.isConnected) { + await delay(500); + } + } else { + peer.connect(otherId); + } + + alert('Connected!'); + alert('Starting messaging server'); + alert('Respond with empty text to stop'); + + while (true) { + const message = prompt('Message to send:', ''); + if (!message) { + break; + } + peer.send(message); + delay(1000); + } +} +// doStuff(); diff --git a/src/peering_.ts b/src/peering_.ts new file mode 100644 index 0000000..e89878a --- /dev/null +++ b/src/peering_.ts @@ -0,0 +1,119 @@ +let pc; +let localStream; + +const signaling = new BroadcastChannel('webrtc'); +signaling.onmessage = e => { + if (!localStream) { + console.log('not ready yet'); + return; + } + switch (e.data.type) { + case 'offer': + handleOffer(e.data); + break; + case 'answer': + handleAnswer(e.data); + break; + case 'candidate': + handleCandidate(e.data); + break; + case 'ready': + // A second tab joined. This tab will initiate a call unless in a call already. + if (pc) { + console.log('already in call, ignoring'); + return; + } + makeCall(); + break; + case 'bye': + if (pc) { + hangup(); + } + break; + default: + console.log('unhandled', e); + break; + } +}; + +export const start = async () => { + localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); + localVideo.srcObject = localStream; + + signaling.postMessage({type: 'ready'}); +}; + +hangupButton.onclick = async () => { + hangup(); + signaling.postMessage({type: 'bye'}); +}; + +async function hangup() { + if (pc) { + pc.close(); + pc = null; + } + localStream.getTracks().forEach(track => track.stop()); + localStream = null; + startButton.disabled = false; + hangupButton.disabled = true; +}; + +function createPeerConnection() { + pc = new RTCPeerConnection(); + pc.onicecandidate = e => { + const message = { + type: 'candidate', + candidate: null, + }; + if (e.candidate) { + message.candidate = e.candidate.candidate; + message.sdpMid = e.candidate.sdpMid; + message.sdpMLineIndex = e.candidate.sdpMLineIndex; + } + signaling.postMessage(message); + }; + pc.ontrack = e => remoteVideo.srcObject = e.streams[0]; + localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); +} + +async function makeCall() { + await createPeerConnection(); + + const offer = await pc.createOffer(); + signaling.postMessage({type: 'offer', sdp: offer.sdp}); + await pc.setLocalDescription(offer); +} + +async function handleOffer(offer) { + if (pc) { + console.error('existing peerconnection'); + return; + } + await createPeerConnection(); + await pc.setRemoteDescription(offer); + + const answer = await pc.createAnswer(); + signaling.postMessage({type: 'answer', sdp: answer.sdp}); + await pc.setLocalDescription(answer); +} + +async function handleAnswer(answer) { + if (!pc) { + console.error('no peerconnection'); + return; + } + await pc.setRemoteDescription(answer); +} + +async function handleCandidate(candidate) { + if (!pc) { + console.error('no peerconnection'); + return; + } + if (!candidate.candidate) { + await pc.addIceCandidate(null); + } else { + await pc.addIceCandidate(candidate); + } +} \ No newline at end of file diff --git a/src/piano.ts b/src/piano.ts index d9072ec..fb1a7ac 100644 --- a/src/piano.ts +++ b/src/piano.ts @@ -1,36 +1,21 @@ -import { PeerComm } from "./peering"; -import { Move } from "./types"; +// import { PeerComm } from "./peering"; +// import { Move } from "./types"; -export interface Game { - moveHistory: Move[]; - isRedTurn: boolean; - isLocalRed: boolean; - isMoveLegal: (sourceIndex: number, destIndex: number) => boolean; - requestMove: () => ((sourceIndex: number, destIndex: number) => void); -} -enum State { - Starting, - RedTurn, - BlueTurn -} -interface PeerData { - senderId: string; - receiverId: string; - redId: string; - state: State; - move: Move | null; -} +// interface PeerData { +// action: 'move'; +// sourceIndex: number; +// destIndex: number; +// } -export class Piano { - public game: Game; - private peerComm: PeerComm<PeerData>; - private receiver: (data: PeerData) => void; - public constructor(game: Game) { - this.game = game; - this.receiver = (data: PeerData) => { - const isGameStarting = game.moveHistory.length === 0; - const isLocalRed = data.redId === this.peerComm.id; - }; - this.peerComm = new PeerComm<PeerData>(this.receiver) - } -} \ No newline at end of file +// export class Piano { +// public game: IGame; +// private peerComm: PeerComm<PeerData>; +// public constructor(game: IGame) { +// this.game = game; +// this.receiver = (data: PeerData) => { +// const isGameStarting = game.moveHistory.length === 0; +// const isLocalRed = data.redId === this.peerComm.id; +// }; +// this.peerComm = new PeerComm<PeerData>(this.receiver) +// } +// } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 18cf62d..a680fdd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,68 +38,89 @@ export class Coord { } export class Move { - public readonly sourceIndex: number; - public readonly destIndex: number; - public readonly isPieceRed: boolean; - constructor(isPieceRed: boolean, sourceIndex: number, destIndex: number) { - this.sourceIndex = sourceIndex; - this.destIndex = destIndex; - this.isPieceRed = isPieceRed; + public readonly source: Coord; + public readonly dest: Coord; + public readonly playerId: string; + constructor(playerId: string, source: Coord, dest: Coord) { + this.source = source; + this.dest = dest; + this.playerId = playerId; } } -interface Listener { - element: Element; - type: string; - listener: (event: Event) => void; +export interface MoveResult { + isTerminal: boolean; + message?: string; } -export class ListenerManager { - private listeners: Listener[] = []; - - public addListener(element: Element, type: string, listener: (event: Event) => void): void { - const { listeners } = this; - element.addEventListener(type, listener); - listeners.push({ element, type, listener }); - } - - public removeListeners() { - const { listeners } = this; - listeners.forEach(value => { - const { element, type, listener } = value; - element.removeEventListener(type, listener); - }); - listeners.length = 0; - } +export interface IGame { + moveHistory: Move[]; + currentPlayerId: string; + getValidMoves: (start: Coord) => Coord[]; + makeMove: (source: Coord, dest: Coord) => MoveResult; + undoMove: () => void; + get canUndoMove(): boolean; } export class GameBoard { - public board: number[]; - public rows: number; - public cols: number; + private board: (string | null)[]; + public readonly rows: number; + public readonly cols: number; + public readonly player1Id: string; + public readonly player2Id: string + // testBoard: [ // 4, null, null, null, // null, 0, 1, 5, // null, 2, 3, null, // 6, null, null, 7 // ], - public constructor() { + public constructor(player1Id: string, player2Id: string) { this.board = [ - 0, null, null, 4, - null, 1, 5, null, - null, 6, 2, null, - 7, null, null, 3 + player1Id, null, null, player2Id, + null, player1Id, player2Id, null, + null, player2Id, player1Id, null, + player2Id, null, null, player1Id ]; this.rows = 4; this.cols = 4; + this.player1Id = player1Id; + this.player2Id = player2Id; } - public coordToBoardIndex({ row, col }: Coord): number { + public move(source: Coord, dest: Coord): void { + const isP1 = this.get(source); + if (isP1 === null) throw `Source ${source} points to an empty space`; + this.set(source, null); + this.set(dest, isP1); + } + public get(coord: Coord): boolean | null { + const index = this.coordToBoardIndex(coord); + if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`; + const id = this.board[index]; + const isP1 = id === null ? null : (id === this.player1Id); + return isP1; + } + public getCoords(id: string): Coord[] { + const coords = this.board + .map((pieceId, index) => { + const isTarget = pieceId === id; + return isTarget ? this.boardIndexToCoord(index) : null + }) + .filter(coord => coord !== null); + return coords; + } + private set(coord: Coord, isP1: boolean | null): void { + const index = this.coordToBoardIndex(coord); + if (!Number.isSafeInteger(index) || index < 0 || index >= this.board.length) throw `Index ${index} is out of bounds`; + this.board[index] = isP1 ? this.player1Id : this.player2Id; + } + private coordToBoardIndex({ row, col }: Coord): number { const index = row * 4 + col; return index; } - public boardIndexToCoord(index: number): Coord { + private boardIndexToCoord(index: number): Coord { const col = index % 4; const row = Math.floor(index / 4); return new Coord(row, col); } -} \ No newline at end of file +}