fuzzyworld

dao-game / files / src / game / game.ts

game.ts

5.8 KB · history · blame

1import { GameBoard } from "./game-board";
2import { Coord } from "./coord";
3
4export interface Move {
5 source: Coord;
6 dest: Coord;
7 playerId: string;
8}
9
10export interface MoveResult {
11 isTerminal: boolean;
12 message?: string;
13}
14
15export interface IGame {
16 size: Coord;
17 moveHistory: Move[];
18 player1Id: string;
19 player1Name: string;
20 player2Id: string;
21 player2Name: string;
22 currentPlayerId: string;
23 canUndoMove: boolean;
24 getPlayerPositions: (playerId: string) => Coord[];
25 coordToBoardIndex: (coord: Coord) => number;
26 getValidMoves: (start: Coord) => Coord[];
27 makeMove: (source: Coord, dest: Coord) => Promise<MoveResult>;
28 undoMove: () => Move;
29 getPiece(pos: Coord): string | undefined;
30}
31
32export class Game implements IGame {
33 private isGameOver: boolean;
34 private readonly gameBoard: GameBoard;
35 private readonly _moveHistory: Move[];
36 public readonly player1Id: string;
37 public readonly player2Id: string;
38 public readonly player1Name: string;
39 public readonly player2Name: string;
40
41 public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string) {
42 this.gameBoard = new GameBoard(player1Id, player2Id);
43 this.player1Id = player1Id;
44 this.player2Id = player2Id;
45 this.player1Name = player1Name;
46 this.player2Name = player2Name;
47 this.isGameOver = false;
48 this._moveHistory = [];
49 this.currentPlayerId = player1Id;
50 }
51 getPiece(pos: Coord): string {
52 const value = this.gameBoard.get(pos);
53 if (value === null) return null;
54 const b = value.valueOf();
55 const id = b ? this.player1Id : this.player2Id;
56 return id;
57 }
58 public currentPlayerId: string;
59 public get size(): Coord {
60 return this.gameBoard.size;
61 }
62 public get moveHistory(): Move[] {
63 return this._moveHistory;
64 }
65 public getPlayerPositions(playerId: string): Coord[] {
66 return this.gameBoard.getCoords(playerId);
67 }
68 public coordToBoardIndex(coord: Coord): number {
69 return this.gameBoard.coordToBoardIndex(coord);
70 }
71 public async makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
72 const checkForWin = (isP1Turn: boolean): MoveResult => {
73 const doCoordsFormSquare = (coords: Coord[]): boolean => {
74 const allRows = coords.map((c) => c.row);
75 const allCols = coords.map((c) => c.col);
76 const min = new Coord(Math.min(...allRows), Math.min(...allCols));
77 const max = new Coord(Math.max(...allRows), Math.max(...allCols));
78 const diff = max.sub(min);
79 const isSquare = diff.row === 1 && diff.col === 1;
80 return isSquare;
81 };
82 const { gameBoard, player1Id, player2Id, player1Name, player2Name } = this;
83 const playerId = isP1Turn ? player1Id : player2Id;
84
85 const playerCoords = gameBoard.getCoords(playerId);
86 const squareCoords: Coord[] = [new Coord(0, 0), new Coord(3, 0), new Coord(3, 3), new Coord(0, 3)];
87
88 // winning conditions:
89 // 1. all in one row
90 // 2. all in one column
91 // 3. all in corners
92 // 4. all in square
93 // losing conditions:
94 // 1. on three sides of an opponent
95
96 const firstPiece = playerCoords[0];
97 const allInRow = playerCoords.every((pc) => pc.row === firstPiece.row);
98 const allInCol = playerCoords.every((pc) => pc.col === firstPiece.col);
99 const allInCorners = playerCoords.every((pc) => squareCoords.findIndex((sc) => pc.equals(sc)) >= 0);
100 const allInSquare = doCoordsFormSquare(playerCoords);
101
102 const winnerName = isP1Turn ? player1Name : player2Name;
103 let winningMessage = undefined;
104 if (allInRow) {
105 winningMessage = `${winnerName} wins by forming a row`;
106 }
107 if (allInCol) {
108 winningMessage = `${winnerName} wins by forming a column`;
109 }
110 if (allInCorners) {
111 winningMessage = `${winnerName} wins by putting pieces in corners`;
112 }
113 if (allInSquare) {
114 winningMessage = `${winnerName} wins by forming a square`;
115 }
116 if (winningMessage) {
117 return {
118 isTerminal: true,
119 message: winningMessage,
120 };
121 } else {
122 return {
123 isTerminal: false,
124 };
125 }
126 };
127
128 const { gameBoard, player1Id, player2Id } = this;
129 const isP1Turn = this.currentPlayerId === player1Id;
130 const playerId = isP1Turn ? player1Id : player2Id;
131
132 this._moveHistory.push({ playerId, source, dest });
133 gameBoard.move(source, dest);
134
135 const result = checkForWin(isP1Turn);
136 if (result.isTerminal) {
137 this.isGameOver = true;
138 } else {
139 this.currentPlayerId = isP1Turn ? this.player2Id : this.player1Id;
140 }
141 return result;
142 }
143 public getValidMoves(start: Coord): Coord[] {
144 const getFarthestCoord = (startCoord: Coord, direction: Coord): Coord => {
145 const { gameBoard } = this;
146 let farthestPos = undefined;
147 let testPos = startCoord;
148 while (true) {
149 const pos = new Coord(testPos.row + direction.row, testPos.col + direction.col);
150
151 const withinBounds = gameBoard.size.contains(pos);
152 if (!withinBounds) {
153 break;
154 }
155 const isOccupied = gameBoard.get(pos) !== null;
156 if (isOccupied) {
157 break;
158 }
159
160 farthestPos = pos;
161 testPos = pos;
162 }
163 return farthestPos;
164 };
165
166 const farthestCoords = Coord.allDirections
167 .map((direction) => getFarthestCoord(start, direction))
168 .filter((farCoord) => farCoord !== undefined);
169
170 return farthestCoords;
171 }
172 public undoMove(): Move {
173 if (!this.canUndoMove) {
174 throw "Newp! Can't undo!";
175 }
176 const { gameBoard } = this;
177 const move = this._moveHistory.pop();
178 const { source, dest, playerId } = move;
179 this.currentPlayerId = playerId;
180 gameBoard.move(dest, source);
181 return move;
182 }
183 public get canUndoMove(): boolean {
184 if (this._moveHistory.length === 0 || this.isGameOver) {
185 return false;
186 }
187 return true;
188 }
189}