PageSourceSearch

https://matthewshang.com/blog/nim-chaos/nim.js

js matthewshang.com collected 2026-10-04 03:51:34 UTC 6,330 bytes, 257 lines download raw bytes

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.