-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
308 lines (282 loc) · 8.82 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
/**
* An agent that produces a move given the board.
*/
class Agent {
/**
* Calculates the best move and returns it.
*
* @param {Array} heaps A list of remaining chips in each heap
* @returns {object} The heap to remove from and how much to remove
*/
static nextMove(heaps) {
const heapsCopy = Array.from(heaps.entries()).sort((a,b) => b[1] - a[1]);
const nimSum = heapsCopy.reduce((total, element) => total ^ element[1]);
for (let i = 0; i < heapsCopy.length && heapsCopy[i][1] !== 0; i++) {
for (let rem = 0; rem < heapsCopy[i][1]; rem++) {
if ((rem ^ nimSum) === 0) {
return {index: heapsCopy[i][0], amount: heapsCopy[i][1] - rem};
}
}
}
return {index: heapsCopy[0][0], amount: 1};
}
}
/**
* The board of the game. It holds the
* actual drawing and what to draw.
*/
class Board {
/**
* Create new board.
*
* @param {object} canvas The canvas DOM element
* @param {Array} heaps An array of how many chips are in each heap
*/
constructor(canvas, heaps) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.heaps = heaps;
this.playersTurn = true;
}
/**
* Used to seperate rectangles when drawn. If you wanna
* change this, keep it between 0 and 1 (exclusively).
*
* @returns {number} A offset to seperate rectangles when drawn
*/
static offset() {
return 0.05;
}
/**
* Find the tallest heap.
*
* @return {int} The number of chips in the largest heap.
*/
getMaxHeight() {
return Math.max(...this.heaps);
}
/**
* How much horizontal space a rectangle can take up of the canvas.
*
* @return {number} The max width of rectangles
*/
getDeltaX() {
return this.canvas.width / this.heaps.length;
}
/**
* How much vertical space a ractangle can take up of the canvas.
*
* @param {int} maxHeight The size of the tallest heap.
* @return {number} The max height of rectangles
*/
getDeltaY(maxHeight) {
return this.canvas.height / (maxHeight ? maxHeight : this.getMaxHeight());
}
/**
* Sets attributes of the canvas and its context.
*/
adjustCanvas() {
this.canvas.width = Math.round(window.innerWidth * 0.8);
this.canvas.height = Math.round(window.innerHeight * 0.8);
this.ctx.fillStyle="#FF0000";
this.ctx.font = "30px sans-serif";
this.ctx.textBaseline = 'middle';
this.ctx.textAlign = "center";
}
/**
* Draws all the rectangles (chips) to the canvas.
*/
plotRectangles() {
const maxHeight = this.getMaxHeight();
const dx = this.getDeltaX();
const dy = this.getDeltaY(maxHeight);
for (let i = 0; i < this.heaps.length; i++) {
for (let j = 0; j < this.heaps[i]; j++) {
this.ctx.fillRect(
(i + Board.offset())* dx,
((maxHeight - j - 1) + Board.offset()) * dy,
(1 - 2 * Board.offset()) * dx,
(1 - 2 * Board.offset()) * dy
);
}
}
}
/**
* Draw the state of the game to the canvas.
*/
redraw() {
this.adjustCanvas();
if (this.heaps.length > 0) {
this.plotRectangles();
} else {
// If the game is over
const results = !this.playersTurn ? 'Win!' : 'Lose!';
this.ctx.fillText("Game Over - You " + results, this.canvas.width / 2, this.canvas.height / 2);
}
}
/**
* Find which heap, if any, the click belongs to.
*
* @param {int} x The x coordinate (of a mouse click)
* @returns {int} The index of the heap being clicked or -1 if none.
*/
findHeap(x) {
const dx = this.getDeltaX();
for (let i = 0; i < this.heaps.length; i++) {
const minX = (i + Board.offset()) * dx;
const maxX = minX + (1 - 2 * Board.offset()) * dx;
if (minX <= x && x <= maxX) {
return i;
}
}
return -1;
}
/**
* Find which chip (and those above) should be removed, with regards to a click.
*
* @param {int} y The y coordinate (of a mouse click)
* @param {Array} heap The heaps currently in play
* @returns {int} The amount that should be removed from a given heap or -1 if none.
*/
findAmount(y, heap) {
const maxHeight = this.getMaxHeight();
const dy = this.getDeltaY(maxHeight);
for (let j = 0; j < this.heaps[heap]; j++) {
let a = ((maxHeight - j - 1) + Board.offset()) * dy;
let b = ((maxHeight - j - 1) + Board.offset()) * dy + (1 - 2 * Board.offset()) * dy;
if (a <= y && b >= y) {
return this.heaps[heap] - j;
}
}
return -1;
}
/**
* Validates a click and removes the chips the player asked for.
*
* @param {int} x The x coordinate (of a mouse click)
* @param {int} y The y coordinate (of a mouse click)
* @returns {boolean} true iff successful
*/
click(x,y) {
let index = this.findHeap(x);
if (index !== -1) {
let amount = this.findAmount(y, index);
if (amount !== -1) {
return this.remove(index, amount);
}
}
return false;
}
/**
* The computer's removal of chips.
*/
computerPlay() {
const move = Agent.nextMove(this.heaps);
this.remove(move.index, move.amount);
}
/**
* Removes chips from heap. If the heap is empty
* after the removal, it is also removed.
*
* @param {int} index The index of heap to remove from
* @param {int} amount The amount of chips to remove from the heap
* @returns {boolean} true iff successful
*/
remove(index, amount) {
if (index >= 0 && index < this.heaps.length && this.heaps[index] >= amount) {
this.heaps[index] -= amount;
if (this.heaps[index] === 0) {
this.heaps.splice(index, 1);
}
this.playersTurn = !this.playersTurn;
this.redraw();
return true;
}
this.redraw();
return false;
}
}
/**
* A class that holds the board and players move.
*/
class Game {
/**
* Create a new game. Each heap is set to a
* random number inclusively between 5 and 15.
*
* @param {object} canvas The canvas DOM element
* @param {int} numberOfHeaps The number of heaps in this game
*/
constructor(canvas, numberOfHeaps) {
this.board = new Board(canvas, Array.from({length: numberOfHeaps}, () => Math.floor(Math.random() * 11) + 5));
this.board.redraw();
}
/**
* Check if game is over.
*
* @returns {boolean} true if no moves left to make
*/
isOver() {
return this.board.heaps.length === 0;
}
/**
* One move from the human player by clicking (x,y)
* coordinate of the canvas. If successful, it also
* tells the computer it can make its move.
*
* @param {int} x The horizontal coordinate of a click
* @param {int} y The vertical coordinate of a click
*/
playerMove(x, y) {
if (this.isOver() || !this.board.playersTurn) {
return;
}
if (this.board.click(x, y)) {
this.computerMove();
}
}
/**
* Make a move as the computer.
*/
computerMove() {
if (!this.isOver()) {
this.board.computerPlay();
}
}
}
/*
* Function scope wrapper.
*/
(function() {
let canvas = document.getElementById("canvas");
let heapCount = 3;
let game = new Game(canvas, heapCount);
// Resize event redraws the canvas
window.addEventListener('resize', (evt) => game.board.redraw());
// Key events for new game or different heap counts
window.addEventListener('keypress', (evt) => {
switch (evt.key.toUpperCase()) {
case 'N':
game = new Game(canvas, heapCount);
break;
case 'ARROWLEFT':
if (heapCount > 3) {
heapCount--;
game = new Game(canvas, heapCount);
}
break;
case 'ARROWRIGHT':
if (heapCount < 7) {
heapCount++;
game = new Game(canvas, heapCount);
}
break;
}
});
// On click event for player to remove from heaps
canvas.addEventListener('mousedown', (evt) => {
if (!game.isOver() && game.board.playersTurn) {
game.playerMove(evt.offsetX, evt.offsetY);
}
});
})();