fuzzyworld

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

local-game.ts

8.4 KB · history · blame

1import { Game, IGame, MoveResult } from './game';
2import { ListenerManager } from '../utils/listeners';
3import { showModal } from '../utils/modal';
4import { Coord } from './coord';
5
6const classNames = {
7 selectedPiece: 'selected-piece',
8 selectedPlayerText: 'selected-player-text',
9 activePlayerPiece: 'active-player-piece',
10 possibleMove: 'possible-move',
11};
12
13export class LocalGame extends Game {
14 selectedPiece: Element | null = null;
15 listenerManager: ListenerManager = new ListenerManager();
16
17 // DOM references
18 readonly dom = {
19 defaultP1Piece: document.getElementById('default-p1-piece'),
20 defaultP2Piece: document.getElementById('default-p2-piece'),
21 resetButton: document.getElementById('reset-button') as HTMLInputElement,
22 undoButton: document.getElementById('undo-button') as HTMLInputElement,
23 cells: () => Array.from(document.querySelectorAll('td')),
24 allPieces: () => Array.from(document.querySelectorAll('.piece')),
25 p1TurnText: () => Array.from(document.querySelectorAll('.p1-turn-text'))[0],
26 p2TurnText: () => Array.from(document.querySelectorAll('.p2-turn-text'))[0],
27 };
28
29 private getPiecesAndCoords(playerId: string) {
30 const allCells = this.dom.cells();
31 const playerPositions = this.getPlayerPositions(playerId);
32 const piecesAndPositions = playerPositions.map((coord) => {
33 const index = this.coordToBoardIndex(coord);
34 const cell = allCells[index];
35 const piece = cell.children[0];
36 return { coord, piece };
37 });
38 return piecesAndPositions;
39 }
40
41 /**
42 * Animates the motion of piece moving from source to dest
43 * @param source Starting Coord
44 * @param dest Ending Coord
45 * @param playerId Player id
46 */
47 private async movePiece(source: Coord, dest: Coord) {
48 const { dom } = this;
49 const sourceIndex = this.coordToBoardIndex(source);
50 const destIndex = this.coordToBoardIndex(dest);
51
52 // find current piece
53 const piece = dom.cells()[sourceIndex].children[0];
54 // unselect piece
55 piece.classList.remove(classNames.selectedPiece);
56
57 // create clone for new position
58 const destPiece = piece.cloneNode(true);
59 // add to destination cell
60 dom.cells()[destIndex].appendChild(destPiece);
61
62 // start animations
63 const animDurationMs = 500;
64 const destElement = dom.cells()[destIndex];
65 const destAnimation = destElement.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animDurationMs, iterations: 1 });
66 const sourceAnimation = piece.animate(
67 [
68 {
69 opacity: 1,
70 width: '45px',
71 height: '45px',
72 },
73 {
74 opacity: 0,
75 width: '5px',
76 height: '5px',
77 },
78 ],
79 { duration: animDurationMs, iterations: 1 }
80 );
81
82 // await animations
83 await Promise.all([sourceAnimation.finished, destAnimation.finished]);
84
85 // remove old piece
86 piece.parentNode.removeChild(piece);
87 }
88
89 public constructor(player1Id: string, player1Name: string, player2Id: string, player2Name: string) {
90 super(player1Id, player1Name, player2Id, player2Name);
91 this.init();
92 }
93
94 public override async makeMove(source: Coord, dest: Coord): Promise<MoveResult> {
95 this.listenerManager.removeListeners();
96 await this.movePiece(source, dest);
97 const result = await super.makeMove(source, dest);
98 this.handleMoveResult(result);
99 return result;
100 }
101
102 public async handleMoveResult(result: MoveResult) {
103 const { isTerminal, message } = result;
104 if (isTerminal) {
105 await showModal({ text: message });
106 document.location.reload();
107 } else {
108 this.resetUi();
109 this.setActiveUi();
110 }
111 }
112
113 public resetUi() {
114 const { dom, currentPlayerId, player1Id, canUndoMove, moveHistory, listenerManager, selectedPiece } = this;
115
116 // reset text
117 dom.p1TurnText().classList.remove(classNames.selectedPlayerText);
118 dom.p2TurnText().classList.remove(classNames.selectedPlayerText);
119
120 const isP1Turn = currentPlayerId === player1Id;
121 if (isP1Turn) {
122 dom.p1TurnText().classList.add(classNames.selectedPlayerText);
123 } else {
124 dom.p2TurnText().classList.add(classNames.selectedPlayerText);
125 }
126
127 if (dom.undoButton || dom.resetButton) {
128 dom.undoButton.disabled = canUndoMove;
129 if (moveHistory.length === 0) {
130 dom.undoButton.disabled = true;
131 dom.resetButton.disabled = true;
132 } else {
133 dom.undoButton.disabled = false;
134 dom.resetButton.disabled = false;
135 }
136 }
137
138 this.selectedPiece = null;
139
140 // reset Cells
141 dom.cells().forEach((cell) => cell.classList.remove(classNames.possibleMove));
142 // reset pieces
143 dom.allPieces().forEach((piece) => {
144 piece.classList.remove(classNames.activePlayerPiece);
145 piece.classList.remove(classNames.selectedPiece);
146 });
147 // remove all listeners
148 listenerManager.removeListeners();
149 }
150 public setActiveUi() {
151 const { dom, currentPlayerId, listenerManager } = this;
152
153 const clearPossibleMovesBlink = () =>
154 dom.cells().forEach((cell) => cell.classList.remove(classNames.possibleMove));
155 const activePieceListener = (piece: Element, source: Coord) => {
156 // clear current selection
157 if (this.selectedPiece !== null) {
158 this.selectedPiece.classList.remove(classNames.selectedPiece);
159 clearPossibleMovesBlink();
160 listenerManager.removeListeners();
161 hookupActivePieces();
162 }
163
164 // set as selected
165 this.selectedPiece = piece;
166 piece.classList.add(classNames.selectedPiece);
167
168 // hook up allowed moves for:
169 // 1. blink
170 // 2. complete the move
171 const allowedMoves = this.getValidMoves(source);
172 allowedMoves.forEach((dest: Coord) => {
173 const cell = dom.cells()[this.coordToBoardIndex(dest)];
174 cell.classList.add(classNames.possibleMove);
175 listenerManager.addListener(cell, 'click', async () => {
176 clearPossibleMovesBlink();
177 await this.makeMove(source, dest);
178 });
179 });
180 };
181
182 const hookupActivePieces = () => {
183 // mark active pieces
184 const piecesAndPositions = this.getPiecesAndCoords(currentPlayerId);
185 piecesAndPositions.forEach(({ coord: source, piece }) => {
186 piece.classList.add(classNames.activePlayerPiece);
187 listenerManager.addListener(piece, 'click', () => activePieceListener(piece, source));
188 });
189 }
190
191 hookupActivePieces();
192 }
193
194 private init() {
195 const { dom, player1Id, player1Name, player2Id, player2Name, size } = this;
196
197 // create board and pieces
198 let isWhite = true;
199 const table = document.getElementById('table');
200 for (let r = 0; r < size.row; r++) {
201 const tr = document.createElement('tr');
202 table.appendChild(tr);
203 for (let c = 0; c < size.col; c++) {
204 const td = document.createElement('td');
205 tr.appendChild(td);
206 td.classList.add('tile');
207 td.classList.add(isWhite ? 'white' : 'black');
208 isWhite = !isWhite;
209 }
210 isWhite = !isWhite;
211 }
212 const createPiece = (isP1Turn: boolean, pieceId: string, coord: Coord) => {
213 const defaultPiece = isP1Turn ? dom.defaultP1Piece : dom.defaultP2Piece;
214 const newPiece = defaultPiece.cloneNode(true) as HTMLParagraphElement;
215 newPiece.id = pieceId;
216 const index = this.coordToBoardIndex(coord);
217 dom.cells()[index].appendChild(newPiece);
218 };
219 this.getPlayerPositions(player1Id).forEach((coord) => createPiece(true, player1Id, coord));
220 this.getPlayerPositions(player2Id).forEach((coord) => createPiece(false, player2Id, coord));
221
222 dom.p1TurnText().textContent = `${player1Name}`
223 dom.p2TurnText().textContent = `${player2Name}`
224
225 // hook up buttons
226 let blockActions = false;
227 if (dom.resetButton) {
228 dom.resetButton.addEventListener('click', () => {
229 if (blockActions) {
230 return;
231 }
232
233 blockActions = true;
234 if (confirm('Reset?')) {
235 window.location.reload();
236 }
237 blockActions = false;
238 });
239 }
240
241 if (dom.undoButton) {
242 dom.undoButton.addEventListener('click', async () => {
243 if (blockActions) {
244 return;
245 }
246
247 blockActions = true;
248
249 const move = this.undoMove();
250 await this.movePiece(move.dest, move.source);
251 this.resetUi();
252 this.setActiveUi();
253
254 blockActions = false;
255 });
256 }
257 }
258}
259
260// start!
261export const playGame = () => {
262 const lg = new LocalGame('red-player', 'Red', 'blue-player', 'Blue');
263 lg.resetUi();
264 lg.setActiveUi();
265};