├── 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 |
--------------------------------------------------------------------------------