├── index.html └── pegsolitaire.js /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Peg Solitaire 6 | 7 | 8 | 9 |

Peg Solitaire

10 | 11 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pegsolitaire.js: -------------------------------------------------------------------------------- 1 | (function(global){ 2 | if(!global.misohena){global.misohena = {};} 3 | if(!global.misohena.js_pegsolitaire){global.misohena.js_pegsolitaire = {};} 4 | var mypkg = global.misohena.js_pegsolitaire; 5 | 6 | 7 | // 8 | // Model 9 | // 10 | 11 | var INVALID_HOLE_ID = -1; 12 | var INVALID_DIR = -1; 13 | 14 | var MAX_BOARD_SIZE = 50; 15 | 16 | function BoardBase() 17 | { 18 | this.pushPeg = function(holeId){ 19 | this.setPegExists(holeId, true); 20 | return this; 21 | }; 22 | this.pullPeg = function(holeId){ 23 | this.setPegExists(holeId, false); 24 | return this; 25 | }; 26 | this.movePeg = function(fromId, toId){ 27 | if(this.hasPeg(fromId) && this.hasEmptyHole(toId)){ 28 | var dir = this.getDirFromToDist2(fromId, toId); 29 | if(dir != INVALID_DIR){ 30 | var nextId = this.getAdjacent(fromId, dir); 31 | var nextNextId = this.getAdjacent(nextId, dir); 32 | if(this.hasPeg(nextId)){ 33 | this.pullPeg(fromId); 34 | this.pullPeg(nextId); 35 | this.pushPeg(nextNextId); 36 | return true; 37 | } 38 | } 39 | } 40 | return false; 41 | }; 42 | this.undoMovePeg = function(fromId, toId){ 43 | if(this.hasEmptyHole(fromId) && this.hasPeg(toId)){ 44 | var dir = this.getDirFromToDist2(fromId, toId); 45 | if(dir != INVALID_DIR){ 46 | var nextId = this.getAdjacent(fromId, dir); 47 | var nextNextId = this.getAdjacent(nextId, dir); 48 | if(this.hasEmptyHole(nextId)){ 49 | this.pushPeg(fromId); 50 | this.pushPeg(nextId); 51 | this.pullPeg(nextNextId); 52 | return true; 53 | } 54 | } 55 | } 56 | return false; 57 | }; 58 | this.canMoveFrom = function(fromId){ 59 | if(this.hasPeg(fromId)){ 60 | for(var dir = 0; dir < this.getDirCount(); ++dir){ 61 | if(this.canMoveDir(fromId, dir)){ 62 | return true; 63 | } 64 | } 65 | } 66 | return false; 67 | }; 68 | this.canMoveFromTo = function(fromId, toId){ 69 | if(this.hasPeg(fromId) && this.hasEmptyHole(toId)){ 70 | return this.hasPeg( 71 | this.getAdjacent(fromId, 72 | this.getDirFromToDist2(fromId, toId))); 73 | } 74 | return false; 75 | }; 76 | this.canMoveDir = function(fromId, dir){ 77 | var nextId = this.getAdjacent(fromId, dir); 78 | var nextNextId = this.getAdjacent(nextId, dir); 79 | return this.hasPeg(fromId) && 80 | this.hasPeg(nextId) && 81 | this.hasEmptyHole(nextNextId); 82 | }; 83 | this.getDirFromTo = function(fromId, toId){ 84 | for(var dir = 0; dir < this.getDirCount(); ++dir){ 85 | var id = this.getAdjacent(fromId, dir); 86 | while(this.hasValidHole(id)){ 87 | if(id == toId){ 88 | return dir; 89 | } 90 | id = this.getAdjacent(id, dir); 91 | } 92 | } 93 | return INVALID_DIR; 94 | }; 95 | this.getDirFromToDist2 = function(fromId, toId){ 96 | if(this.hasValidHole(fromId) && this.hasValidHole(toId)){ 97 | for(var dir = 0; dir < this.getDirCount(); ++dir){ 98 | var nextNextId = this.getAdjacent(this.getAdjacent(fromId, dir), dir); 99 | if(nextNextId == toId){ 100 | return dir; 101 | } 102 | } 103 | } 104 | return INVALID_DIR; 105 | }; 106 | this.findHoleAtPosition = function(x, y, r, includingInvalidHoles){ 107 | if(!r){ r = 0.5;} 108 | var count = this.getHoleCount(); 109 | for(var id = 0; id < count; ++id){ 110 | if(includingInvalidHoles || this.hasValidHole(id)){ 111 | var dx = this.getHoleLayoutPositionX(id) - x; 112 | var dy = this.getHoleLayoutPositionY(id) - y; 113 | if(dx*dx+dy*dy < r*r){ 114 | return id; 115 | } 116 | } 117 | } 118 | return INVALID_HOLE_ID; 119 | }; 120 | this.getPegCount = function(){ 121 | var holeCount = this.getHoleCount(); 122 | var pegCount = 0; 123 | for(var id = 0; id < holeCount; ++id){ 124 | if(this.hasPeg(id)){ 125 | ++pegCount; 126 | } 127 | } 128 | return pegCount; 129 | }; 130 | this.isSolved = function(){ 131 | return this.getPegCount() == 1; 132 | }; 133 | this.isEnd = function(){ 134 | var holeCount = this.getHoleCount(); 135 | for(var id = 0; id < holeCount; ++id){ 136 | if(this.hasPeg(id)){ 137 | if(this.canMoveFrom(id)){ 138 | return false; 139 | } 140 | } 141 | } 142 | return true; 143 | }; 144 | this.eachHole = function(fun, includingInvalidHoles){ 145 | var holeCount = this.getHoleCount(); 146 | for(var id = 0; id < holeCount; ++id){ 147 | if(includingInvalidHoles || this.hasValidHole(id)){ 148 | fun(id); 149 | } 150 | } 151 | }; 152 | } 153 | 154 | function GridBoardBase(holes) 155 | { 156 | BoardBase.call(this); 157 | 158 | // Board Interface 159 | 160 | this.getHoleCount = function(){ 161 | return holes.length; 162 | }; 163 | this.hasValidHole = function(holeId){ 164 | return holes[holeId] !== undefined; 165 | }; 166 | this.hasEmptyHole = function(holeId){ 167 | return holes[holeId] === false; 168 | }; 169 | this.hasPeg = function(holeId){ 170 | return holes[holeId] === true; 171 | }; 172 | 173 | this.setHoleState = function(holeId, stateUndefinedOrFlaseOrTrue){ 174 | if(holeId >= 0 && holeId < holes.length){ 175 | holes[holeId] = typeof(stateUndefinedOrFlaseOrTrue) == "boolean" ? stateUndefinedOrFlaseOrTrue : undefined; 176 | } 177 | }; 178 | this.getHoleState = function(holeId){ 179 | return holes[holeId]; 180 | }; 181 | this.setPegExists = function(holeId, peg){ 182 | if(this.hasValidHole(holeId)){ 183 | holes[holeId] = peg === true; 184 | } 185 | return this; 186 | }; 187 | this.setHoleOpen = function(holeId, open){ 188 | if(holeId >= 0 && holeId < holes.length){ 189 | if(open){ 190 | holes[holeId] = false; 191 | } 192 | else{ 193 | holes[holeId] = undefined; 194 | } 195 | } 196 | return this; 197 | }; 198 | this.clear = function(){ 199 | for(var id = 0; id < holes.length; ++id){ 200 | this.setHoleState(id, undefined); 201 | } 202 | return this; 203 | }; 204 | this.boreHoleAll = function(){ 205 | for(var id = 0; id < holes.length; ++id){ 206 | this.setHoleOpen(id, true); 207 | } 208 | return this; 209 | }; 210 | this.fillPegAll = function(){ 211 | for(var id = 0; id < holes.length; ++id){ 212 | this.setPegExists(id, true); 213 | } 214 | return this; 215 | }; 216 | this.getHolesString = function(){ 217 | return GridBoardBase.convertHolesToString(holes); 218 | }; 219 | } 220 | GridBoardBase.convertHolesToString = function(holes){ 221 | var str = ""; 222 | for(var id = 0; id < holes.length; ++id){ 223 | var h = holes[id]; 224 | str += h === true ? "P" : h === false ? "O" : "_"; 225 | } 226 | return str; 227 | }; 228 | GridBoardBase.convertStringToHoles = function(str){ 229 | var holes = []; 230 | for(var i = 0; i < str.length; ++i){ 231 | var c = str.charAt(i); 232 | holes.push(c == "P" ? true : c == "O" ? false : undefined); 233 | } 234 | return holes; 235 | }; 236 | 237 | 238 | mypkg.RectangularBoard = RectangularBoard; 239 | function RectangularBoard(w, h, holes) 240 | { 241 | // ex) w=6, h=3 242 | // 0 1 2 3 4 5 243 | // 6 7 8 9 10 11 244 | // 12 13 14 15 16 17 245 | if(!holes) { holes = new Array(w*h);} 246 | GridBoardBase.call(this, holes); 247 | 248 | // Board Interface 249 | 250 | this.xy = function(x, y){ 251 | if(x >= 0 && x < w && y >= 0 && y < h){ 252 | return x + y * w; 253 | } 254 | else{ 255 | return INVALID_HOLE_ID; 256 | } 257 | }; 258 | this.getAdjacent = function(holeId, dir){ 259 | if(this.hasValidHole(holeId)){ 260 | switch(dir){ 261 | case 0: return toX(holeId)+1 < w ? holeId+1 : INVALID_HOLE_ID; 262 | case 1: return toY(holeId)+1 < h ? holeId+w : INVALID_HOLE_ID; 263 | case 2: return toX(holeId) > 0 ? holeId-1 : INVALID_HOLE_ID; 264 | case 3: return toY(holeId) > 0 ? holeId-w : INVALID_HOLE_ID; 265 | } 266 | } 267 | return INVALID_HOLE_ID; 268 | }; 269 | this.getDirCount = function(){ 270 | return 4; 271 | }; 272 | this.getHoleLayoutPositionX = function(holeId){ 273 | return toX(holeId); 274 | }; 275 | this.getHoleLayoutPositionY = function(holeId){ 276 | return toY(holeId); 277 | }; 278 | this.getLayoutSizeX = function(){ 279 | return w-1; 280 | }; 281 | this.getLayoutSizeY = function(){ 282 | return h-1; 283 | }; 284 | this.getWidth = function(){ return w;}; 285 | this.getHeight = function(){ return h;}; 286 | this.getSize = function(){ return Math.max(w, h);}; 287 | this.getType = function(){ return RectangularBoard.TYPEID;}; 288 | this.toString = function(){ 289 | return this.getType() + " " + w + " " + h + " " + this.getHolesString(); 290 | }; 291 | this.copyFrom = function(from, left, top){ 292 | for(var y = 0; y < h; ++y){ 293 | for(var x = 0; x < w; ++x){ 294 | this.setHoleState(this.xy(x, y), from.getHoleState(from.xy(left+x, top+y))); 295 | } 296 | } 297 | }; 298 | 299 | // Rectangular Only 300 | 301 | this.fillRect = function(rectX, rectY, rectW, rectH, state){ 302 | if(rectW <= 0 || rectH <= 0){ 303 | return this; 304 | } 305 | var holeId = rectX + rectY * w; 306 | for(var yc = rectH; yc > 0; --yc){ 307 | for(var xc = rectW; xc > 0; --xc){ 308 | holes[holeId] = state; 309 | ++holeId; 310 | } 311 | holeId += w - rectW; 312 | } 313 | return this; 314 | }; 315 | 316 | function toX(holeId){ return holeId % w;} 317 | function toY(holeId){ return Math.floor(holeId / w);} 318 | } 319 | RectangularBoard.TYPEID = "R"; 320 | 321 | mypkg.HexGridBoard = HexGridBoard; 322 | function HexGridBoard(w, h, holes) 323 | { 324 | // ex)w=4,h=3 325 | // 0 1 2 3 326 | // 4 5 6 7 327 | // 8 9 10 11 328 | RectangularBoard.call(this, w, h, holes); 329 | 330 | // Board Interface 331 | this.getAdjacent = function(holeId, dir){ 332 | if(this.hasValidHole(holeId)){ 333 | var x = toX(holeId); 334 | var y = toY(holeId); 335 | switch(dir){ 336 | case 0: return fromXY(x+1,y); 337 | case 1: return (y&1)==0 ? fromXY(x,y+1) : fromXY(x+1,y+1); 338 | case 2: return (y&1)==0 ? fromXY(x-1,y+1) : fromXY(x,y+1); 339 | case 3: return fromXY(x-1,y); 340 | case 4: return (y&1)==0 ? fromXY(x-1,y-1) : fromXY(x,y-1); 341 | case 5: return (y&1)==0 ? fromXY(x,y-1) : fromXY(x+1,y-1); 342 | } 343 | } 344 | return INVALID_HOLE_ID; 345 | }; 346 | this.getDirCount = function(){ 347 | return 6; 348 | }; 349 | this.getHoleLayoutPositionX = function(holeId){ 350 | return toX(holeId) + (toY(holeId) & 1) * 0.5; 351 | }; 352 | this.getHoleLayoutPositionY = function(holeId){ 353 | return toY(holeId); 354 | }; 355 | this.getLayoutSizeX = function(){ 356 | return (w-1) + (h > 1 ? 0.5 : 0); 357 | }; 358 | this.getLayoutSizeY = function(){ 359 | return h-1; 360 | }; 361 | this.getType = function(){ return HexGridBoard.TYPEID;}; 362 | this.toString = function(){ 363 | return this.getType() + " " + w + " " + h + " " + this.getHolesString(); 364 | }; 365 | 366 | // 367 | 368 | function fromXY(x, y){ 369 | return (x >= 0 && y >= 0 && x < w && y < h) ? x+y*w : INVALID_HOLE_ID; 370 | } 371 | function toX(holeId){ return holeId % w;} 372 | function toY(holeId){ return Math.floor(holeId / w);} 373 | } 374 | HexGridBoard.TYPEID = "H"; 375 | 376 | mypkg.TriangularBoard = TriangularBoard; 377 | function TriangularBoard(size, holes) 378 | { 379 | // ex)size=4 380 | // 0 381 | // 1 2 382 | // 3 4 5 383 | // 6 7 8 9 384 | if(!holes) { holes = new Array((size*(size+1))/2);} 385 | GridBoardBase.call(this, holes); 386 | 387 | // Board Interface 388 | 389 | this.xy = function(x, y){ 390 | return xyToId(x, y); 391 | }; 392 | this.getAdjacent = function(holeId, dir){ 393 | if(this.hasValidHole(holeId)){ 394 | var pos = idToXY(holeId); 395 | var w = pos.y + 1; 396 | switch(dir){ 397 | case 0: return pos.x+1 < w ? holeId+1 : INVALID_HOLE_ID; 398 | case 1: return pos.y+1 < size ? holeId+w+1 : INVALID_HOLE_ID; 399 | case 2: return pos.y+1 < size ? holeId+w : INVALID_HOLE_ID; 400 | case 3: return pos.x > 0 ? holeId-1 : INVALID_HOLE_ID; 401 | case 4: return pos.x > 0 && pos.y > 0 ? holeId-w : INVALID_HOLE_ID; 402 | case 5: return pos.x+1 < w && pos.y > 0 ? holeId-w+1 : INVALID_HOLE_ID; 403 | } 404 | } 405 | return INVALID_HOLE_ID; 406 | }; 407 | this.getDirCount = function(){ 408 | return 6; 409 | }; 410 | this.getHoleLayoutPositionX = function(holeId){ 411 | var pos = idToXY(holeId); 412 | return (size-1)*0.5 - pos.y*0.5 + pos.x; 413 | }; 414 | this.getHoleLayoutPositionY = function(holeId){ 415 | return idToY(holeId); 416 | }; 417 | this.getLayoutSizeX = function(){ 418 | return size-1; 419 | }; 420 | this.getLayoutSizeY = function(){ 421 | return size-1; 422 | }; 423 | this.getWidth = function(){ return size;}; 424 | this.getHeight = function(){ return size;}; 425 | this.getSize = function(){ return size;}; 426 | this.getType = function(){ return TriangularBoard.TYPEID;}; 427 | this.toString = function(){ 428 | return this.getType() + " " + size + " " + this.getHolesString(); 429 | }; 430 | this.copyFrom = function(from, left, top){ 431 | for(var y = 0; y < size; ++y){ 432 | for(var x = 0; x < y+1; ++x){ 433 | this.setHoleState(this.xy(x, y), from.getHoleState(from.xy(left+x, top+y))); 434 | } 435 | } 436 | }; 437 | 438 | // 439 | 440 | function yToId(y){ 441 | if(y >= 0 && y < size){ 442 | return y*(y+1)/2; 443 | } 444 | else{ 445 | return INVALID_HOLE_ID; 446 | } 447 | } 448 | function xyToId(x, y){ 449 | if(y >= 0 && y < size && x >= 0 && x <= y){ 450 | return yToId(y) + x; 451 | } 452 | else{ 453 | return INVALID_HOLE_ID; 454 | } 455 | } 456 | function idToY(holeId){ 457 | return Math.floor((Math.sqrt(1 + 8*holeId) - 1)/2); 458 | } 459 | function idToXY(holeId){ 460 | var y = idToY(holeId); 461 | var x = holeId - yToId(y); 462 | return {x:x, y:y}; 463 | } 464 | function idToX(holeId){ 465 | var y = idToY(holeId); 466 | return holeId - yToId(y); 467 | } 468 | } 469 | TriangularBoard.TYPEID = "T"; 470 | 471 | function parseBoard(str) 472 | { 473 | function createBoardWidthHeight(ctor, lines){ 474 | var w = parseInt(lines[1], 10); 475 | var h = parseInt(lines[2], 10); 476 | var holesStr = lines[3]; 477 | if(!(w >= 0 && w < MAX_BOARD_SIZE) || !(h >= 0 && h < MAX_BOARD_SIZE) || holesStr.length != w*h){ 478 | return null; 479 | } 480 | var holes = GridBoardBase.convertStringToHoles(holesStr); 481 | return new ctor(w, h, holes); 482 | } 483 | function createBoardTriangle(ctor, lines){ 484 | var size = parseInt(lines[1], 10); 485 | var holesStr = lines[2]; 486 | if(!(size >= 0 && size < MAX_BOARD_SIZE) || holesStr.length != (size*(size+1))/2){ 487 | return null; 488 | } 489 | var holes = GridBoardBase.convertStringToHoles(holesStr); 490 | return new ctor(size, holes); 491 | } 492 | var lines = str.split(/\s+/); 493 | var ctor; 494 | var args = []; 495 | switch(lines[0]){ 496 | case RectangularBoard.TYPEID: 497 | return createBoardWidthHeight(RectangularBoard, lines); 498 | case HexGridBoard.TYPEID: 499 | return createBoardWidthHeight(HexGridBoard, lines); 500 | case TriangularBoard.TYPEID: 501 | return createBoardTriangle(TriangularBoard, lines); 502 | default: 503 | return null; 504 | } 505 | } 506 | 507 | mypkg.createEnglishBoard = createEnglishBoard; 508 | function createEnglishBoard() 509 | { 510 | var board = new RectangularBoard(7,7); 511 | board.fillRect(2,0,3,7, true); 512 | board.fillRect(0,2,7,3, true); 513 | board.pullPeg(board.xy(3,3)); 514 | return board; 515 | } 516 | 517 | mypkg.createEuropeanBoard = createEuropeanBoard; 518 | function createEuropeanBoard() 519 | { 520 | var board = new RectangularBoard(7,7); 521 | board.fillRect(2,0,3,7, true); 522 | board.fillRect(0,2,7,3, true); 523 | board.fillRect(1,1,5,5, true); 524 | board.pullPeg(board.xy(3,3)); 525 | return board; 526 | } 527 | 528 | mypkg.createTriangular5Board = createTriangular5Board; 529 | function createTriangular5Board() 530 | { 531 | var board = new TriangularBoard(5); 532 | board.boreHoleAll(); 533 | board.fillPegAll(); 534 | board.pullPeg(board.xy(0,0)); 535 | return board; 536 | } 537 | 538 | mypkg.createHexagonal5Board = createHexagonal5Board; 539 | function createHexagonal5Board() 540 | { 541 | var board = new HexGridBoard(9, 9); 542 | board.fillRect(2,0,5,1, true); 543 | board.fillRect(1,1,6,1, true); 544 | board.fillRect(1,2,7,1, true); 545 | board.fillRect(0,3,8,1, true); 546 | board.fillRect(0,4,9,1, true); 547 | board.fillRect(0,5,8,1, true); 548 | board.fillRect(1,6,7,1, true); 549 | board.fillRect(1,7,6,1, true); 550 | board.fillRect(2,8,5,1, true); 551 | board.pullPeg(board.xy(4,4)); 552 | return board; 553 | } 554 | 555 | function createPropellerBoard() 556 | { 557 | var board = new HexGridBoard(5, 5); 558 | board.fillRect(1,0,3,1, true); 559 | board.fillRect(1,1,2,1, true); 560 | board.fillRect(0,2,5,1, true); 561 | board.fillRect(0,3,4,1, true); 562 | board.fillRect(1,4,1,1, true); 563 | board.fillRect(3,4,1,1, true); 564 | board.pullPeg(board.xy(2,2)); 565 | return board; 566 | } 567 | 568 | mypkg.createMinimumBoard = createMinimumBoard; 569 | function createMinimumBoard() 570 | { 571 | var board = new RectangularBoard(3,1); 572 | board.boreHoleAll(); 573 | board.fillPegAll(); 574 | board.pullPeg(board.xy(0,0)); 575 | return board; 576 | } 577 | 578 | mypkg.create4HolesBoard = create4HolesBoard; 579 | function create4HolesBoard() 580 | { 581 | var board = new RectangularBoard(4,1); 582 | board.boreHoleAll(); 583 | board.fillPegAll(); 584 | board.pullPeg(board.xy(1,0)); 585 | return board; 586 | } 587 | 588 | mypkg.create5HolesBoard = create5HolesBoard; 589 | function create5HolesBoard() 590 | { 591 | var board = new RectangularBoard(3,3); 592 | board.fillRect(0,0,3,1, true); 593 | board.fillRect(1,1,1,2, true); 594 | board.pullPeg(board.xy(2,0)); 595 | return board; 596 | } 597 | 598 | 599 | mypkg.History = History; 600 | function History() 601 | { 602 | var moves = []; 603 | this.add = function(from, to){ 604 | moves.push({from:from, to:to}); 605 | }; 606 | this.undo = function(board){ 607 | if(moves.length > 0){ 608 | var lastMove = moves.pop(); 609 | board.undoMovePeg(lastMove.from, lastMove.to); 610 | } 611 | }; 612 | this.getMoveCount = function(){return moves.length;}; 613 | this.clear = function(){ 614 | moves.splice(0, moves.length); 615 | }; 616 | } 617 | 618 | 619 | 620 | // 621 | // View/Control 622 | // 623 | 624 | function drawBoardToCanvas(canvas, ctx, board, opt, draggingPeg, drawInvalidHoles) 625 | { 626 | ctx.clearRect(0,0,canvas.width, canvas.height); 627 | var left = opt.paddingLeft; 628 | var top = opt.paddingTop; 629 | var holeSpanX = opt.holeSpanX; 630 | var holeSpanY = opt.holeSpanY; 631 | var holeRadius = opt.holeRadius; 632 | var pegRadius = opt.pegRadius; 633 | 634 | // Invalid Holes 635 | if(drawInvalidHoles){ 636 | board.eachHole(function(holeId){ 637 | if(!board.hasValidHole(holeId)){ 638 | var holeX = left + board.getHoleLayoutPositionX(holeId) * holeSpanX; 639 | var holeY = top + board.getHoleLayoutPositionY(holeId) * holeSpanY; 640 | ctx.beginPath(); 641 | ctx.moveTo(holeX-pegRadius, holeY); 642 | ctx.lineTo(holeX+pegRadius, holeY); 643 | ctx.moveTo(holeX, holeY-pegRadius); 644 | ctx.lineTo(holeX, holeY+pegRadius); 645 | ctx.strokeStyle = "black"; 646 | ctx.lineWidth = 1; 647 | ctx.stroke(); 648 | } 649 | }, true); 650 | } 651 | 652 | // Hole 653 | board.eachHole(function(holeId){ 654 | var holeX = left + board.getHoleLayoutPositionX(holeId) * holeSpanX; 655 | var holeY = top + board.getHoleLayoutPositionY(holeId) * holeSpanY; 656 | ctx.beginPath(); 657 | ctx.arc(holeX, holeY, holeRadius, 0, Math.PI*2, false); 658 | if(draggingPeg && holeId == draggingPeg.getDstHoleId() && board.canMoveFromTo(draggingPeg.getHoleId(), holeId)){ 659 | ctx.strokeStyle = "red"; 660 | ctx.lineWidth = 3; 661 | } 662 | else{ 663 | ctx.strokeStyle = "black"; 664 | ctx.lineWidth = 1; 665 | } 666 | ctx.stroke(); 667 | }); 668 | 669 | // Peg 670 | board.eachHole(function(holeId){ 671 | if(board.hasPeg(holeId)){ 672 | var pegX = left + board.getHoleLayoutPositionX(holeId) * holeSpanX; 673 | var pegY = top + board.getHoleLayoutPositionY(holeId) * holeSpanY; 674 | if(draggingPeg && holeId == draggingPeg.getHoleId()){ 675 | pegX += draggingPeg.getDeltaX(); 676 | pegY += draggingPeg.getDeltaY(); 677 | } 678 | ctx.beginPath(); 679 | ctx.arc(pegX, pegY, pegRadius, 0, Math.PI*2, false); 680 | ctx.fillStyle = "black"; 681 | ctx.fill(); 682 | } 683 | }); 684 | } 685 | 686 | mypkg.createCanvasView = createCanvasView; 687 | function createCanvasView(board) 688 | { 689 | var history = new History(); 690 | var HOLE_SPAN = 48; 691 | var opt = { 692 | paddingLeft: HOLE_SPAN*0.5, 693 | paddingTop: HOLE_SPAN*0.5, 694 | paddingRight: HOLE_SPAN*0.5, 695 | paddingBottom: HOLE_SPAN*0.5, 696 | holeSpanX: HOLE_SPAN, 697 | holeSpanY: HOLE_SPAN, 698 | holeRadius: HOLE_SPAN*0.375, 699 | pegRadius: HOLE_SPAN*0.3125 700 | }; 701 | 702 | var canvas = document.createElement("canvas"); 703 | canvas.setAttribute("width", opt.paddingLeft + board.getLayoutSizeX() * opt.holeSpanX + opt.paddingRight); 704 | canvas.setAttribute("height", opt.paddingTop + board.getLayoutSizeY() * opt.holeSpanY + opt.paddingBottom); 705 | 706 | function update() 707 | { 708 | drawBoardToCanvas( 709 | canvas, 710 | canvas.getContext("2d"), 711 | board, 712 | opt, 713 | draggingPeg, 714 | getMode() == MODE_EDIT ? true : false); 715 | } 716 | 717 | // 718 | // Board 719 | // 720 | 721 | function move(fromId, toId) 722 | { 723 | if(board.movePeg(fromId, toId)){ 724 | history.add(fromId, toId); 725 | update(); 726 | fireBoardMovedEvent(); 727 | } 728 | } 729 | function undo() 730 | { 731 | history.undo(board); 732 | update(); 733 | } 734 | function fireBoardMovedEvent() 735 | { 736 | var ev = document.createEvent("HTMLEvents"); 737 | ev.initEvent("boardmoved", true, false); 738 | canvas.dispatchEvent(ev); 739 | } 740 | 741 | // 742 | // Input 743 | // 744 | 745 | var draggingPeg = null; 746 | function DraggingPeg(holeId, initialMousePos) 747 | { 748 | var deltaPos = {x:0, y:0}; 749 | var dstHoleId = INVALID_HOLE_ID; 750 | 751 | this.getHoleId = function() { return holeId;}; 752 | this.setMousePosition = function(pos, dstId) { 753 | deltaPos.x = pos.x - initialMousePos.x; 754 | deltaPos.y = pos.y - initialMousePos.y; 755 | dstHoleId = dstId; 756 | }; 757 | this.getDeltaX = function(){ return deltaPos.x;}; 758 | this.getDeltaY = function(){ return deltaPos.y;}; 759 | this.getDstHoleId = function(){ return dstHoleId;}; 760 | } 761 | 762 | function mousePosToHoleId(xy, includingInvalidHoles) 763 | { 764 | return board.findHoleAtPosition( 765 | (xy.x - opt.paddingLeft) / opt.holeSpanX, 766 | (xy.y - opt.paddingTop) / opt.holeSpanY, 767 | undefined, 768 | includingInvalidHoles); 769 | } 770 | 771 | function PlayingMode() 772 | { 773 | this.leaveMode = function() 774 | { 775 | this.onMouseLeave(); 776 | }; 777 | this.onMouseDown = function(ev) 778 | { 779 | var pos = getMouseEventPositionOnElement(canvas, ev); 780 | var holeId = mousePosToHoleId(pos); 781 | if(board.hasPeg(holeId)){ 782 | draggingPeg = new DraggingPeg(holeId, pos); 783 | update(); 784 | } 785 | }; 786 | this.onMouseMove = function(ev) 787 | { 788 | if(draggingPeg){ 789 | var pos = getMouseEventPositionOnElement(canvas, ev); 790 | var holeId = mousePosToHoleId(pos); 791 | draggingPeg.setMousePosition(pos, holeId); 792 | update(); 793 | } 794 | }; 795 | this.onMouseUp = function(ev) 796 | { 797 | if(draggingPeg){ 798 | var dstHoleId = draggingPeg.getDstHoleId(); 799 | if(board.hasEmptyHole(dstHoleId)){ 800 | move(draggingPeg.getHoleId(), dstHoleId); 801 | } 802 | draggingPeg = null; 803 | update(); 804 | } 805 | }; 806 | this.onMouseLeave = function(ev) 807 | { 808 | if(draggingPeg){ 809 | draggingPeg = null; 810 | update(); 811 | } 812 | }; 813 | } 814 | function EditingMode() 815 | { 816 | var lastHoleState = null; 817 | 818 | this.leaveMode = function() 819 | { 820 | this.onMouseLeave(); 821 | }; 822 | this.onMouseDown = function(ev) 823 | { 824 | var pos = getMouseEventPositionOnElement(canvas, ev); 825 | var holeId = mousePosToHoleId(pos, true); 826 | if(holeId >= 0 && holeId < board.getHoleCount()){ 827 | var oldHoleState = board.getHoleState(holeId); 828 | var newHoleState = 829 | oldHoleState === undefined ? true : 830 | oldHoleState === true ? false : 831 | undefined; 832 | board.setHoleState(holeId, newHoleState); 833 | update(); 834 | lastHoleState = newHoleState; 835 | } 836 | }; 837 | this.onMouseMove = function(ev) 838 | { 839 | if(lastHoleState !== null){ 840 | var pos = getMouseEventPositionOnElement(canvas, ev); 841 | var holeId = mousePosToHoleId(pos, true); 842 | if(holeId >= 0 && holeId < board.getHoleCount()){ 843 | board.setHoleState(holeId, lastHoleState); 844 | update(); 845 | } 846 | } 847 | }; 848 | this.onMouseUp = function(ev) 849 | { 850 | if(lastHoleState !== null){ 851 | lastHoleState = null; 852 | } 853 | }; 854 | this.onMouseLeave = function(ev) 855 | { 856 | if(lastHoleState !== null){ 857 | lastHoleState = null; 858 | } 859 | }; 860 | } 861 | var MODE_PLAY = "Playing"; 862 | var MODE_EDIT = "Editing"; 863 | var modeObj = new PlayingMode(); 864 | var modeName = MODE_PLAY; 865 | function setMode(modeStr) 866 | { 867 | var modeCtor = 868 | modeStr==MODE_PLAY ? PlayingMode : 869 | modeStr==MODE_EDIT ? EditingMode : 870 | null; 871 | if(!modeCtor){ 872 | return; 873 | } 874 | modeObj.leaveMode(); 875 | modeObj = new modeCtor(); 876 | modeName = modeStr; 877 | update(); 878 | } 879 | function getMode() 880 | { 881 | return modeName; 882 | } 883 | 884 | 885 | function onMouseDown(ev){ modeObj.onMouseDown(ev);} 886 | function onMouseMove(ev){ modeObj.onMouseMove(ev);} 887 | function onMouseUp(ev){ modeObj.onMouseUp(ev);} 888 | function onMouseLeave(ev){ modeObj.onMouseLeave(ev);} 889 | function onTouchStart(ev) 890 | { 891 | onMouseDown(ev.touches[0]); 892 | ev.preventDefault(); 893 | } 894 | function onTouchMove(ev) 895 | { 896 | onMouseMove(ev.touches[0]); 897 | ev.preventDefault(); 898 | } 899 | function onTouchEnd(ev) 900 | { 901 | onMouseUp(); 902 | ev.preventDefault(); 903 | } 904 | 905 | canvas.addEventListener("mousedown", onMouseDown, false); 906 | canvas.addEventListener("mousemove", onMouseMove, false); 907 | canvas.addEventListener("mouseup", onMouseUp, false); 908 | canvas.addEventListener("mouseleave", onMouseLeave, false); 909 | canvas.addEventListener("touchstart", onTouchStart, false); 910 | canvas.addEventListener("touchmove", onTouchMove, false); 911 | canvas.addEventListener("touchend", onTouchEnd, false); 912 | 913 | // Public Interface 914 | 915 | canvas.pegsolitaire = { 916 | update: update, 917 | undo: undo, 918 | history: history, 919 | board: board, 920 | setMode: setMode, 921 | getMode: getMode, 922 | MODE_PLAY: MODE_PLAY, 923 | MODE_EDIT: MODE_EDIT 924 | }; 925 | 926 | update(); 927 | return canvas; 928 | } 929 | 930 | mypkg.getBoardCatalog = getBoardCatalog; 931 | function getBoardCatalog(){ 932 | return [ 933 | {id:"English", ctor:createEnglishBoard, title:"English Style(33 holes)"}, 934 | {id:"European", ctor:createEuropeanBoard, title:"European Style(37 holes)"}, 935 | {id:"Triangular5", ctor:createTriangular5Board, title:"Triangular5(15 holes)"}, 936 | {id:"Hexagonal5", ctor:createHexagonal5Board, title:"Hexagonal5(61 holes)"}, 937 | {id:"Propeller", ctor:createPropellerBoard, title:"Propeller(16 holes)"}, 938 | {id:"Minimum", ctor:createMinimumBoard, title:"Minimum(3 holes)"}, 939 | {id:"4Holes", ctor:create4HolesBoard, title:"4Holes(4 holes)"}, 940 | {id:"5Holes", ctor:create5HolesBoard, title:"5Holes(5 holes)"}, 941 | {id:"Easy Pinwheel", str:"R 4 4 __P_OPP__PPP_P__", title:"Easy Pinwheel(8 holes)"}, 942 | {id:"Banzai7", str:"H 3 3 OPOPP__PP", title:"Banzai7(7 holes)"}, 943 | {id:"Megaphone", str:"H 4 4 _P__PPPP__PP__O_", title:"Megaphone(8 holes)"}, 944 | {id:"Owl", str:"H 4 4 _PPPPOOP_PPP_PP_", title:"Owl(12 holes)"}, 945 | {id:"Star", str:"H 4 5 __O_PPPP_PPPPPPP__P_", title:"Star(13 holes)"}, 946 | {id:"Arrow9", str:"H 4 4 __P_OPP__PPP_PP_", title:"Arrow9(9 holes)"} 947 | ]; 948 | }; 949 | 950 | mypkg.createGameBox = createGameBox; 951 | function createGameBox(opt) 952 | { 953 | if(!opt){ 954 | opt = {}; 955 | } 956 | 957 | var catalog = opt.catalog || getBoardCatalog(); 958 | if(opt.boardText){ 959 | catalog.splice(0, 0, {id:"Default", ctor:function(){return parseBoard(opt.boardText);}, title:"Default"}); 960 | } 961 | 962 | var gameDiv = newElem("div"); 963 | 964 | // control 965 | 966 | var controlDiv = newElem("div", gameDiv); 967 | 968 | var boardCtors = {}; 969 | var selectBoard = null; 970 | if(!opt.disableCatalogSelect){ 971 | selectBoard = newElem("select", controlDiv); 972 | for(var i = 0; i < catalog.length; ++i){ 973 | var option = newElem("option", selectBoard); 974 | option.setAttribute("value", catalog[i].id); 975 | option.appendChild(document.createTextNode(catalog[i].title)); 976 | boardCtors[catalog[i].id] = catalog[i].ctor || 977 | (function(str){ 978 | return function(){return parseBoard(str);}; 979 | })(catalog[i].str); 980 | } 981 | } 982 | 983 | if(!opt.disableNewGame){ 984 | newButton(controlDiv, "New Game", newGame); 985 | } 986 | if(!opt.disableUndo){ 987 | newButton(controlDiv, "Undo", undo); 988 | } 989 | if(!opt.disableEdit){ 990 | newButton(controlDiv, "Edit", edit); 991 | } 992 | 993 | // status 994 | 995 | var statusDiv = newElem("div", gameDiv); 996 | var spanMoves = newElem("span", statusDiv); 997 | statusDiv.appendChild(document.createTextNode(" ")); 998 | var spanGameState = newElem("span", statusDiv); 999 | 1000 | function updateStatus(){ 1001 | if(currentCanvas){ 1002 | spanMoves.innerHTML = "Moves:" + currentCanvas.pegsolitaire.history.getMoveCount(); 1003 | var board = currentCanvas.pegsolitaire.board; 1004 | spanGameState.innerHTML = 1005 | currentCanvas.pegsolitaire.getMode() == currentCanvas.pegsolitaire.MODE_EDIT ? "Editing" : 1006 | board.isSolved() ? "Solved!" : 1007 | board.isEnd() ? "End Game" : 1008 | "Playing"; 1009 | } 1010 | } 1011 | 1012 | // canvas 1013 | 1014 | var currentCanvas = null; 1015 | 1016 | function newBoard(board){ 1017 | if(board){ 1018 | var newCanvas = createCanvasView(board); 1019 | if(currentCanvas){ 1020 | currentCanvas.parentNode.insertBefore(newCanvas, currentCanvas); 1021 | currentCanvas.parentNode.removeChild(currentCanvas); 1022 | } 1023 | else{ 1024 | gameDiv.appendChild(newCanvas); 1025 | } 1026 | currentCanvas = newCanvas; 1027 | 1028 | currentCanvas.addEventListener("boardmoved", onBoardMoved, false); 1029 | updateStatus(); 1030 | } 1031 | } 1032 | function newGame(){ 1033 | var creator = 1034 | selectBoard ? boardCtors[selectBoard.value] : 1035 | catalog.length > 0 ? catalog[0].ctor : 1036 | null; 1037 | if(creator){ 1038 | newBoard(creator()); 1039 | } 1040 | } 1041 | function undo(){ 1042 | if(currentCanvas){ 1043 | currentCanvas.pegsolitaire.undo(); 1044 | updateStatus(); 1045 | } 1046 | } 1047 | function onBoardMoved(ev){ 1048 | updateStatus(); 1049 | } 1050 | 1051 | // Editor 1052 | var editorDiv = null; 1053 | function edit(){ 1054 | currentCanvas.pegsolitaire.setMode(currentCanvas.pegsolitaire.MODE_EDIT); 1055 | updateStatus(); 1056 | 1057 | if(editorDiv){ 1058 | return; 1059 | } 1060 | 1061 | editorDiv = newElem("div", gameDiv); 1062 | newButton(editorDiv, "Play", function(){ 1063 | currentCanvas.pegsolitaire.setMode(currentCanvas.pegsolitaire.MODE_PLAY); 1064 | updateStatus(); 1065 | editorDiv.parentNode.removeChild(editorDiv); 1066 | editorDiv = null; 1067 | }); 1068 | if(opt.enableShare){ 1069 | newButton(editorDiv, "Share", function(){ 1070 | var dlg = newElem("div", editorDiv); 1071 | dlg.appendChild(newTextNode("Share:")); 1072 | newElem("br", dlg); 1073 | dlg.appendChild(newTextNode("URL:")); 1074 | newElem("br", dlg); 1075 | var pageURL = window.location.protocol + "//" + window.location.host + window.location.pathname; 1076 | var urlText = newElem("input", dlg); 1077 | urlText.setAttribute("type", "text"); 1078 | urlText.value = pageURL + "?p=" + currentCanvas.pegsolitaire.board.toString().replace(/ /g, "+"); 1079 | newElem("br", dlg); 1080 | if(opt.scriptURL){ 1081 | dlg.appendChild(newTextNode("Embed Script:")); 1082 | newElem("br", dlg); 1083 | var embedText = newElem("textarea", dlg); 1084 | embedText.setAttribute("rows", "2"); 1085 | embedText.value = 1086 | "\n"+ 1087 | ""; 1091 | newElem("br", dlg); 1092 | } 1093 | newButton(dlg, "Close", function(){ 1094 | closeDlg(); 1095 | }); 1096 | function closeDlg(){ 1097 | dlg.parentNode.removeChild(dlg); 1098 | } 1099 | }); 1100 | } 1101 | newButton(editorDiv, "Export", function(){ 1102 | var dlg = newElem("div", editorDiv); 1103 | dlg.appendChild(document.createTextNode("Export:")); 1104 | var text = newElem("input", dlg); 1105 | text.setAttribute("type", "text"); 1106 | text.value = currentCanvas.pegsolitaire.board.toString(); 1107 | newButton(dlg, "Close", function(){ 1108 | closeDlg(); 1109 | }); 1110 | function closeDlg(){ 1111 | dlg.parentNode.removeChild(dlg); 1112 | } 1113 | }); 1114 | newButton(editorDiv, "Import", function(){ 1115 | var dlg = newElem("div", editorDiv); 1116 | dlg.appendChild(document.createTextNode("Import:")); 1117 | var text = newElem("input", dlg); 1118 | text.setAttribute("type", "text"); 1119 | newButton(dlg, "OK", function(){ 1120 | importBoard(text.value); 1121 | closeDlg(); 1122 | }); 1123 | newButton(dlg, "Cancel", closeDlg); 1124 | function importBoard(str){ 1125 | newBoard(parseBoard(str)); 1126 | } 1127 | function closeDlg(){ 1128 | dlg.parentNode.removeChild(dlg); 1129 | } 1130 | }); 1131 | newButton(editorDiv, "Clear History", function(){ 1132 | if(currentCanvas){ 1133 | currentCanvas.pegsolitaire.history.clear(); 1134 | updateStatus(); 1135 | } 1136 | }); 1137 | newButton(editorDiv, "Clear Board", function(){ 1138 | if(currentCanvas){ 1139 | currentCanvas.pegsolitaire.board.clear(); 1140 | currentCanvas.pegsolitaire.update(); 1141 | updateStatus(); 1142 | } 1143 | }); 1144 | newButton(editorDiv, "Resize", function(){ 1145 | var BOARD_TYPES = [ 1146 | {id:RectangularBoard.TYPEID, title:"Rectangular", pget:function(b){return ["w", b.getWidth(), "h", b.getHeight()];}, creator: function(props){return new RectangularBoard(props.w, props.h);}}, 1147 | {id:HexGridBoard.TYPEID, title:"HexGrid", pget: function(b){return ["w", b.getWidth(), "h", b.getHeight()];}, creator: function(props){return new HexGridBoard(props.w, props.h);}}, 1148 | {id:TriangularBoard.TYPEID, title:"Triangular", pget: function(b){return ["size", b.getSize()];}, creator: function(props){return new TriangularBoard(props.size);}} 1149 | ]; 1150 | var BOARD_TYPES_DIC = {}; 1151 | 1152 | var dlg = newElem("div", editorDiv); 1153 | dlg.appendChild(document.createTextNode("Resize:")); 1154 | var selectType = newElem("select", dlg); 1155 | for(var oi = 0; oi < BOARD_TYPES.length; ++oi){ 1156 | var bt = BOARD_TYPES[oi]; 1157 | BOARD_TYPES_DIC[bt.id] = bt; 1158 | var option = newElem("option", selectType); 1159 | option.setAttribute("value", bt.id); 1160 | option.appendChild(newTextNode(bt.title)); 1161 | } 1162 | selectType.addEventListener("change", function(ev){ 1163 | updatePropElem(); 1164 | }, false); 1165 | var propElem = newElem("span", dlg); 1166 | var propInputs = []; 1167 | var currBoardType = null; 1168 | function updatePropElem(){ 1169 | var newBoardType = BOARD_TYPES_DIC[selectType.value]; 1170 | if(!newBoardType){ 1171 | return; 1172 | } 1173 | while(propElem.firstChild){propElem.removeChild(propElem.firstChild);} 1174 | 1175 | var props = newBoardType.pget(currentCanvas.pegsolitaire.board); 1176 | props.push("dx"); 1177 | props.push(0); 1178 | props.push("dy"); 1179 | props.push(0); 1180 | var inputs = []; 1181 | 1182 | for(var pi = 0; pi < props.length; pi += 2){ 1183 | propElem.appendChild(newTextNode(props[pi] + ":")); 1184 | var input = newElem("input", propElem); 1185 | input.setAttribute("type", "number"); 1186 | input.style.width = "3em"; 1187 | input.value = props[pi+1]; 1188 | inputs.push({name:props[pi], elem:input}); 1189 | } 1190 | propInputs = inputs; 1191 | currBoardType = newBoardType; 1192 | } 1193 | selectType.value = currentCanvas.pegsolitaire.board.getType(); 1194 | updatePropElem(); 1195 | 1196 | newButton(dlg, "OK", function(){ 1197 | if(!currBoardType){ 1198 | return; 1199 | } 1200 | var props = {}; 1201 | for(var ii = 0; ii < propInputs.length; ++ii){ 1202 | props[propInputs[ii].name] = parseInt(propInputs[ii].elem.value, 10); 1203 | } 1204 | var board = currBoardType.creator(props); 1205 | board.copyFrom(currentCanvas.pegsolitaire.board, -props.dx, -props.dy); 1206 | 1207 | newBoard(board); 1208 | currentCanvas.pegsolitaire.setMode(currentCanvas.pegsolitaire.MODE_EDIT); 1209 | closeDlg(); 1210 | }); 1211 | newButton(dlg, "Cancel", closeDlg); 1212 | 1213 | function closeDlg(){ 1214 | dlg.parentNode.removeChild(dlg); 1215 | } 1216 | }); 1217 | } 1218 | 1219 | newGame(); 1220 | return gameDiv; 1221 | } 1222 | 1223 | mypkg.insertGameBoxBeforeCurrentScript = insertGameBoxBeforeCurrentScript; 1224 | function insertGameBoxBeforeCurrentScript(opt) 1225 | { 1226 | var script = getLastScriptNode(); 1227 | var gameBox = createGameBox(opt); 1228 | script.parentNode.insertBefore(gameBox, script); 1229 | return gameBox; 1230 | } 1231 | 1232 | 1233 | // 1234 | // HTML Utility 1235 | // 1236 | mypkg.getLastScriptNode = getLastScriptNode; 1237 | function getLastScriptNode() 1238 | { 1239 | var n = document; 1240 | while(n && n.nodeName.toLowerCase() != "script") { n = n.lastChild;} 1241 | return n; 1242 | } 1243 | 1244 | function getMouseEventPositionOnElement(elem, ev) 1245 | { 1246 | var rect = elem.getBoundingClientRect(); 1247 | return {x:ev.clientX - rect.left, y:ev.clientY - rect.top}; 1248 | } 1249 | 1250 | function newElem(tagName, parentNode) 1251 | { 1252 | var elem = document.createElement(tagName); 1253 | if(parentNode){ 1254 | parentNode.appendChild(elem); 1255 | } 1256 | return elem; 1257 | } 1258 | function newButton(parentNode, value, onClick) 1259 | { 1260 | var button = newElem("input", parentNode); 1261 | button.setAttribute("type", "button"); 1262 | button.setAttribute("value", value); 1263 | button.addEventListener("click", onClick, false); 1264 | return button; 1265 | } 1266 | function newTextNode(text) 1267 | { 1268 | return document.createTextNode(text); 1269 | } 1270 | 1271 | mypkg.getQueryParams = getQueryParams; 1272 | function getQueryParams() 1273 | { 1274 | var result = {}; 1275 | var q = document.location.search.substr(1); 1276 | if(q.length > 0){ 1277 | var ps = q.split("&"); 1278 | for(var pi = 0; pi < ps.length; ++pi){ 1279 | var nv = ps[pi].split("="); 1280 | result[nv[0]] = decodeURI(nv[1].replace(/\+/g, " ")); 1281 | } 1282 | } 1283 | return result; 1284 | } 1285 | 1286 | })(this); 1287 | 1288 | 1289 | 1290 | --------------------------------------------------------------------------------