1function dist2(dx, dy) { 2 return dx * dx + dy * dy; 3} 4 5function randInt(low, high) { 6 return Math.floor(Math.random() * (high - low + 1) + low); 7} 8 9function highBit(x) { 10 for (let i = 10; i >= 0; i--) { 11 if ((x & (1 << i)) > 0) { 12 return i; 13 } 14 } 15 return -1; 16} 17 18document.addEventListener('DOMContentLoaded', function() { 19 let lastMove = document.getElementById('nim-lastmove'); 20 function setStatus(status) { 21 lastMove.innerText = status; 22 } 23 let canvas = document.getElementById('nim-canvas'); 24 let ctx = canvas.getContext('2d'); 25 const logicalWidth = 400; 26 const logicalHeight = 160; 27 let scale = window.devicePixelRatio || 1; 28 canvas.width = logicalWidth * scale; 29 canvas.height = logicalHeight * scale; 30 ctx.scale(scale, scale); 31 let takeButton = document.getElementById('take-button'); 32 let resetButton = document.getElementById('reset-button'); 33 let passButton = document.getElementById('pass-button'); 34 35 const radius = 13; 36 const space = 32; 37 const timeout = 300; 38 39 let piles = []; 40 let selected = 0; 41 let passed = false; 42 let pc = false; 43 44 function bottomSize(pile) { 45 let s = 0; 46 while (pile - s > 0) { 47 pile -= s; 48 s++; 49 } 50 return s; 51 } 52 53 function update() { 54 if (pc) { 55 takeButton.disabled = passButton.disabled = resetButton.disabled = true; 56 } else { 57 takeButton.disabled = selected == 0; 58 passButton.disabled = passed; 59 resetButton.disabled = false; 60 } 61 draw(); 62 } 63 64 function generate(sizes) { 65 piles = []; 66 for (let i = 0; i < sizes.length; i++) { 67 const ox = logicalWidth / (sizes.length + 1) * (i + 1); 68 const s = bottomSize(sizes[i]); 69 let left = sizes[i]; 70 let pile = []; 71 for (let r = 0; left > 0; r++) { 72 const width = s - r; 73 for (let c = 0; c < width && left > 0; c++) { 74 const x = ox - space * (width - 1) / 2 + c * space; 75 const y = logicalHeight - space - Math.sqrt(3) / 2 * space * r; 76 pile.push({x: x, y: y, selected: false}); 77 left--; 78 } 79 } 80 piles.push(pile); 81 } 82 selected = 0; 83 } 84 85 function toggle(pileIdx, stone) { 86 stone.selected = !stone.selected; 87 if (stone.selected) { 88 selected++; 89 } else { 90 selected--; 91 } 92 for (let j = 0; j < piles.length; j++) { 93 if (pileIdx == j) continue; 94 for (let stone of piles[j]) { 95 stone.selected = false; 96 } 97 } 98 } 99 100 function take() { 101 let newSizes = []; 102 let taken = 0; 103 let takenSize = 0; 104 let takenIdx = 0; 105 for (let i = 0; i < piles.length; i++) { 106 let pile = piles[i]; 107 let alive = 0; 108 for (let stone of pile) { 109 if (stone.selected) { 110 taken++; 111 takenSize = pile.length; 112 takenIdx = i; 113 } else { 114 alive++; 115 } 116 } 117 newSizes.push(alive); 118 } 119 120 if (newSizes.length == 0) { 121 setStatus(`${pc ? 'PC' : 'You'} won`); 122 reset(); 123 return true; 124 } else { 125 if (taken == takenSize) { 126 setStatus(`${pc ? 'PC' : 'You'} cleared a pile with ${taken} stone${taken > 1 ? 's' : ''}`); 127 } else { 128 setStatus(`${pc ? 'PC' : 'You'} took ${taken} stone${taken > 1 ? 's' : ''} from pile ${takenIdx}`); 129 } 130 generate(newSizes); 131 update(); 132 return false; 133 } 134 } 135 136 canvas.onclick = function(e) { 137 if (pc) return; 138 const rect = canvas.getBoundingClientRect(); 139 const pointerX = (e.clientX - rect.left) * logicalWidth / rect.width; 140 const pointerY = (e.clientY - rect.top) * logicalHeight / rect.height; 141 for (let i = 0; i < piles.length; i++) { 142 for (let stone of piles[i]) { 143 if (dist2(stone.x - pointerX, stone.y - pointerY) < radius * radius) { 144 toggle(i, stone); 145 } 146 } 147 } 148 update(); 149 } 150 151 takeButton.onclick = function() { 152 if (!take()) { 153 computerMoves(); 154 } 155 } 156 resetButton.onclick = reset; 157 passButton.onclick = function() { 158 setStatus('You passed'); 159 passed = true; 160 for (let pile of piles) { 161 for (let stone of pile) { 162 stone.selected = false;
163 } 164 } 165 update(); 166 computerMoves(); 167 } 168 169 function reset() { 170 generate([9, 2, 4]); 171 // generate([randInt(3, 9), randInt(1, 5), randInt(1, 5)]); 172 passed = false; 173 pc = false; 174 setStatus('Select stones from one pile, then take themâor use the shared pass.'); 175 update(); 176 } 177 178 function draw() { 179 ctx.fillStyle = '#ffffff'; 180 ctx.fillRect(0, 0, logicalWidth, logicalHeight); 181 182 for (let i = 0; i < piles.length; i++) { 183 for (let stone of piles[i]) { 184 if (stone.selected) { 185 ctx.fillStyle = pc ? '#ff0000' : '#0000ff'; 186 } else { 187 ctx.fillStyle = '#000000'; 188 } 189 ctx.beginPath(); 190 ctx.ellipse(stone.x, stone.y, radius, radius, 0, 0, Math.PI * 2); 191 ctx.fill(); 192 } 193 } 194 } 195 196 function randomStrat() { 197 let pileIdx = randInt(0, piles.length - 1); 198 let numTake = randInt(1, piles[pileIdx].length); 199 return {pass: false, pileIdx: pileIdx, numTake: numTake}; 200 } 201 202 function computerStrat() { 203 let xor = 0; 204 for (let pile of piles) { 205 xor ^= pile.length; 206 } 207 208 if (xor == 0) { 209 if (passed) { 210 return randomStrat(); 211 } else { 212 return {pass: true, pileIdx: 0, numTake: 0}; 213 } 214 } else { 215 if (passed) { 216 let high = highBit(xor); 217 for (let i = 0; i < piles.length; i++) { 218 let size = piles[i].length; 219 if (highBit(size) == high) { 220 return {pass: false, pileIdx: i, numTake: size - (size ^ xor)}; 221 } 222 } 223 return randomStrat(); 224 } else { 225 return randomStrat(); 226 } 227 } 228 } 229 230 function computerMoves() { 231 let move = computerStrat(); 232 if (move.pass) { 233 setTimeout(() => { 234 setStatus('PC passed'); 235 passed = true; 236 update(); 237 }, timeout + 500); 238 } else { 239 pc = true; 240 update(); 241 function animate(i) { 242 if (i == move.numTake) { 243 take(); 244 pc = false; 245 update(); 246 } else { 247 toggle(move.pileIdx, piles[move.pileIdx][i]); 248 update(); 249 setTimeout(() => animate(i + 1), timeout); 250 } 251 } 252 setTimeout(() => animate(0), timeout + 500); 253 } 254 } 255 256 reset(); 257});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.