├── README.md ├── index.html ├── canvashelper.js └── squares24.js /README.md: -------------------------------------------------------------------------------- 1 | #GardnerSquaresJs 2 | 3 | Game described by Martin Gardner as "24 Color Squares" 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | McMahon's 24 Color Squares in JavaScript 6 | 7 | 8 | 9 | 10 |
11 |

McMahon's 24 Color Squares

12 |

13 |
Drag squares to swap them, click to rotate.
14 | Goal is to make all the border "triangles" of 15 | the same color,
while pairs of inner "triangles" should match when they touch the same edge.

16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /canvashelper.js: -------------------------------------------------------------------------------- 1 | function CanvasHelper(canvas, context) { 2 | this.canvas = canvas; 3 | this.ctx = context; 4 | this.rndval = 0; 5 | } 6 | 7 | CanvasHelper.prototype.lineRel = function(x1, y1, dx, dy) { 8 | this.line(x1, y1, x1 + dx, y1 + dy); 9 | } 10 | 11 | CanvasHelper.prototype.line = function(x1, y1, x2, y2) { 12 | var ctx = this.ctx; 13 | ctx.beginPath(); 14 | ctx.moveTo(x1, y1); 15 | ctx.lineTo(x2, y2); 16 | ctx.closePath(); 17 | ctx.stroke(); 18 | } 19 | 20 | CanvasHelper.prototype.circle = function(x, y, r) { 21 | var ctx = this.ctx; 22 | ctx.beginPath(); 23 | ctx.arc(x, y, r, 0, 2 * Math.PI, false); 24 | ctx.closePath(); 25 | ctx.fill(); 26 | ctx.stroke(); 27 | } 28 | 29 | CanvasHelper.prototype.posFromEvent = function(e) { 30 | var e = e || window.event; 31 | var cnv = this.canvas; 32 | var offsetX = e.pageX - cnv.clientLeft - cnv.offsetLeft; 33 | var offsetY = e.pageY - cnv.clientTop - cnv.offsetTop; 34 | return {x: offsetX, y: offsetY}; 35 | } 36 | 37 | CanvasHelper.prototype.poly = function(pts) { 38 | var ctx = this.ctx; 39 | ctx.beginPath(); 40 | ctx.moveTo(pts[0][0], pts[0][1]); 41 | for (var i = 1; i < pts.length; i++) { 42 | p = arguments[i]; 43 | ctx.lineTo(pts[i][0], pts[i][1]); 44 | } 45 | ctx.closePath(); 46 | } 47 | 48 | CanvasHelper.prototype.fillPoly = function() { 49 | this.poly(arguments); 50 | this.ctx.fill(); 51 | } 52 | 53 | CanvasHelper.prototype.drawPoly = function() { 54 | this.poly(arguments); 55 | this.ctx.stroke(); 56 | } 57 | 58 | CanvasHelper.prototype.rand = function(seed) { 59 | if (typeof(seed) != 'undefined') { 60 | this.rndval = seed; 61 | } 62 | this.rndval = parseFloat('0.' + Math.sin(this.rndval + 0.31415926).toString().substr(6)); 63 | return this.rndval; 64 | } 65 | -------------------------------------------------------------------------------- /squares24.js: -------------------------------------------------------------------------------- 1 | function Squares24(opts) { 2 | this.w = 6; 3 | this.h = 4; 4 | this.colors = ['#FF0000', '#E0E000', '#3020FF', '#000000']; 5 | this.size = 80; 6 | this.moving = null; 7 | this.rotating = null; 8 | this.infoCallback = null; 9 | if (typeof(opts) == 'object') { 10 | for (var key in opts) { 11 | this[key] = opts[key]; 12 | } 13 | } 14 | this.init(); 15 | } 16 | 17 | Squares24.prototype.init = function() { 18 | var canvas = document.getElementById('demo'); 19 | this.setupGeometry(canvas); 20 | this.ctx = canvas.getContext('2d'); 21 | this.ch = new CanvasHelper(canvas, this.ctx); 22 | this.initialSetup(); 23 | this.draw(); 24 | var self = this; 25 | canvas.onmousedown = function(e) {self.mouseDown(e)}; 26 | canvas.onmouseup = function(e) {self.mouseUp(e)}; 27 | canvas.onmousemove = function(e) {self.mouseMove(e)}; 28 | setInterval(function() {self.animator()}, 50); 29 | } 30 | 31 | Squares24.prototype.initialSetup = function() { 32 | this.genSquares(); 33 | this.shuffle(); 34 | } 35 | 36 | Squares24.prototype.genSquares = function() { 37 | var res = []; 38 | for (var i = 0; i < 81; i++) { 39 | var s = ''; 40 | var p = i; 41 | for (var j = 0; j < 4; j++) { 42 | s += p % 3; 43 | p = Math.floor(p / 3); 44 | } 45 | var best = s; 46 | for (var k = 0; k < 3; k++) { 47 | s = s.substring(1) + s.charAt(0); 48 | if (s < best) { 49 | best = s; 50 | } 51 | } 52 | res[best] = best; 53 | } 54 | this.squares = []; 55 | for (var sq in res) { 56 | this.squares.push(sq); 57 | } 58 | } 59 | 60 | Squares24.prototype.shuffle = function() { 61 | for (var i = 0; i < this.squares.length; i++) { 62 | var j = Math.floor(Math.random() * this.squares.length); 63 | var r = Math.floor(Math.random() * 4); 64 | var t = this.squares[i]; 65 | if (r != 0) { 66 | this.rotate(i); 67 | } 68 | this.squares[i] = this.squares[j]; 69 | this.squares[j] = t; 70 | } 71 | this.recalculate(); 72 | } 73 | 74 | Squares24.prototype.shuffleAndDraw = function() { 75 | this.shuffle(); 76 | this.draw(); 77 | } 78 | 79 | Squares24.prototype.setupGeometry = function(canvas) { 80 | canvas.width = this.size * this.w; 81 | canvas.height = this.size * this.h; 82 | } 83 | 84 | Squares24.prototype.draw = function() { 85 | var ctx = this.ctx; 86 | ctx.fillStyle = this.colors[3]; 87 | ctx.fillRect(0, 0, this.w * this.size, this.h * this.size); 88 | var moving = this.moving; 89 | var rotating = this.rotating; 90 | var skip = moving !== null ? moving.index : (rotating !== null ? rotating.index : -1); 91 | for (var i in this.squares) { 92 | if (i != skip) { 93 | this.drawSquare(i, 0, 0, 0); 94 | } 95 | } 96 | if (moving !== null) { 97 | this.drawSquare(moving.index, moving.dx, moving.dy, 0); 98 | } else if (rotating !== null) { 99 | this.drawSquare(rotating.index, 0, 0, rotating.angle); 100 | } 101 | } 102 | 103 | Squares24.prototype.drawSquare = function(index, dx, dy, da) { 104 | var coords = this.coordsByIndex(index); 105 | var cx = coords[0] + dx; 106 | var cy = coords[1] + dy; 107 | var sq = this.squares[index]; 108 | var stricken = (dx != 0 || dy != 0 || da != 0); 109 | for (var i = 0; i < sq.length; i++) { 110 | this.drawTriangle(coords[0] + dx, coords[1] + dy, i + da, parseInt(sq.charAt(i)), stricken); 111 | } 112 | } 113 | 114 | Squares24.prototype.drawTriangle = function (cx, cy, a, color, stricken) { 115 | var hs = Math.floor(this.size * 0.67); 116 | this.ctx.fillStyle = this.colors[color]; 117 | a /= 2; 118 | var a1 = Math.PI * (a - 0.25); 119 | var a2 = Math.PI * (a + 0.25); 120 | var x1 = cx + hs * Math.cos(a1); 121 | var y1 = cy + hs * Math.sin(a1); 122 | var x2 = cx + hs * Math.cos(a2); 123 | var y2 = cy + hs * Math.sin(a2); 124 | this.ch.fillPoly([cx, cy], [x1, y1], [x2, y2]); 125 | if (stricken) { 126 | this.ctx.lineWidth = 2; 127 | this.ctx.strokeStyle = this.colors[3]; 128 | this.ch.line(x1, y1, x2, y2); 129 | } 130 | } 131 | 132 | Squares24.prototype.coordsByIndex = function(index) { 133 | return [Math.floor(index % this.w) * this.size + Math.floor(this.size / 2), 134 | Math.floor(index / this.w) * this.size + Math.floor(this.size / 2)]; 135 | } 136 | 137 | Squares24.prototype.rotate = function(index) { 138 | var s = this.squares[index]; 139 | this.squares[index] = s.substring(1) + s.charAt(0); 140 | } 141 | 142 | Squares24.prototype.recalculate = function() { 143 | var score = this.recalculateBorder(); 144 | score += this.recalculateHorz(); 145 | score += this.recalculateVert(); 146 | if (typeof(this.infoCallback) == 'function') { 147 | this.infoCallback(score); 148 | } 149 | return score; 150 | } 151 | 152 | Squares24.prototype.recalculateVert = function() { 153 | var sum = 0; 154 | var sq = this.squares; 155 | for (var x = 1; x < this.w; x++) { 156 | for (var y = 0; y < this.h; y++) { 157 | var pos = y * this.w + x; 158 | if (sq[pos - 1].charAt(0) == sq[pos].charAt(2)) { 159 | sum++; 160 | } 161 | } 162 | } 163 | return sum; 164 | } 165 | 166 | Squares24.prototype.recalculateHorz = function() { 167 | var sum = 0; 168 | var sq = this.squares; 169 | for (var y = 1; y < this.h; y++) { 170 | for (var x = 0; x < this.w; x++) { 171 | var pos = y * this.w + x; 172 | if (sq[pos - this.w].charAt(1) == sq[pos].charAt(3)) { 173 | sum++; 174 | } 175 | } 176 | } 177 | return sum; 178 | } 179 | 180 | Squares24.prototype.recalculateBorder = function() { 181 | var cnt = [0, 0, 0]; 182 | var sq = this.squares; 183 | for (var x = 0; x < this.w; x++) { 184 | cnt[parseInt(sq[x].charAt(3))]++; 185 | cnt[parseInt(sq[x + (this.h - 1) * this.w].charAt(1))]++; 186 | } 187 | for (var y = 0; y < this.h; y++) { 188 | cnt[parseInt(sq[y * this.w].charAt(2))]++; 189 | cnt[parseInt(sq[(y + 1) * this.w - 1].charAt(0))]++; 190 | } 191 | var best = 0; 192 | for (var i = 0; i < 3; i++) { 193 | if (cnt[i] > cnt[best]) { 194 | best = i; 195 | } 196 | } 197 | return cnt[best]; 198 | } 199 | 200 | Squares24.prototype.mouseDown = function(event) { 201 | if (this.rotating !== null) { 202 | return; 203 | } 204 | event.preventDefault(); 205 | var pos = this.ch.posFromEvent(event); 206 | pos.ts = new Date().getTime(); 207 | pos.index = this.nearestSquare(pos); 208 | pos.dx = 0; 209 | pos.dy = 0; 210 | this.moving = pos; 211 | } 212 | 213 | Squares24.prototype.mouseUp = function(event) { 214 | event.preventDefault(); 215 | var pos = this.ch.posFromEvent(event); 216 | var sq = this.nearestSquare(pos); 217 | var t = new Date().getTime(); 218 | if (sq == this.moving.index) { 219 | if (t - this.moving.ts < 300) { 220 | this.rotating = {index: sq, angle: 0}; 221 | } 222 | } else { 223 | t = this.squares[sq]; 224 | this.squares[sq] = this.squares[this.moving.index]; 225 | this.squares[this.moving.index] = t; 226 | this.recalculate(); 227 | } 228 | this.moving = null; 229 | this.draw(); 230 | } 231 | 232 | Squares24.prototype.mouseMove = function(event) { 233 | if (this.moving === null) { 234 | return; 235 | } 236 | var pos = this.ch.posFromEvent(event); 237 | var moving = this.moving; 238 | moving.dx = pos.x - moving.x; 239 | moving.dy = pos.y - moving.y; 240 | this.draw(); 241 | } 242 | 243 | Squares24.prototype.animator = function() { 244 | if (this.rotating === null) { 245 | return; 246 | } 247 | this.rotating.angle -= 0.33333333; 248 | if (this.rotating.angle <= -0.9999) { 249 | this.rotate(this.rotating.index); 250 | this.recalculate(); 251 | this.rotating = null; 252 | } 253 | this.draw(); 254 | } 255 | 256 | Squares24.prototype.nearestSquare = function(pos) { 257 | var best = -1; 258 | var bestVal = Infinity; 259 | for (var i in this.squares) { 260 | var coords = this.coordsByIndex(i); 261 | var dist = Math.pow(pos.x - coords[0], 2) + Math.pow(pos.y - coords[1], 2); 262 | if (dist < bestVal) { 263 | best = i; 264 | bestVal = dist; 265 | } 266 | } 267 | return best; 268 | } 269 | --------------------------------------------------------------------------------