├── README.org ├── example ├── common │ ├── bg.jpg │ └── bg5x5_64x64.jpg ├── fixed5x5 │ ├── bg_ELHKB.jpg │ ├── bg_f7BmA.jpg │ ├── bg_qxpwB.jpg │ ├── bg_xBvcA.jpg │ └── index.html ├── index.html ├── random5x5 │ └── index.html ├── server │ └── index.html └── free_mode │ └── index.html ├── LICENSE.txt ├── lightsout.rb └── lightsout.js /README.org: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/README.org -------------------------------------------------------------------------------- /example/common/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/common/bg.jpg -------------------------------------------------------------------------------- /example/fixed5x5/bg_ELHKB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/fixed5x5/bg_ELHKB.jpg -------------------------------------------------------------------------------- /example/fixed5x5/bg_f7BmA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/fixed5x5/bg_f7BmA.jpg -------------------------------------------------------------------------------- /example/fixed5x5/bg_qxpwB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/fixed5x5/bg_qxpwB.jpg -------------------------------------------------------------------------------- /example/fixed5x5/bg_xBvcA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/fixed5x5/bg_xBvcA.jpg -------------------------------------------------------------------------------- /example/common/bg5x5_64x64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misohena/js_lightsout/master/example/common/bg5x5_64x64.jpg -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flip Puzzle Example 6 | 7 | 8 |

Flip Puzzle Example

9 | 10 |

Client Only

11 | 16 |

Client-Server

17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 AKIYAMA Kouhei 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /example/random5x5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Puzzle Random 5x5 Problem Example 6 | 7 | 8 | 9 |

Puzzle Random 5x5 Problem Example

10 | 11 |
12 | 29 | Puzzle Game 30 |
31 |

クライアントサイドでランダム生成した問題を出題する例。

32 |

ご褒美画像のファイル名は常に一定なので、スクリプトを見れば解かずに画像ファイルにアクセスできてしまう。

33 | 34 | 35 | 36 | 37 | 38 |
サーバサイドプログラム不要
出題のバリエーション多様
解かずにファイルへアクセススクリプトを見れば可能
画像URLの共有可能
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/fixed5x5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Puzzle Fixed 5x5 Probrem Example 6 | 7 | 8 | 9 |

Puzzle Fixed 5x5 Probrem Example

10 | 11 |
12 | 41 | Puzzle Game 42 |
43 |

常に同じ問題を出題する例。

44 |

ご褒美画像のファイル名に答えをエンコードした文字列を加えることで、解かないと画像ファイルにアクセスできないようにする。 45 |

答えは複数ありうるので、全ての答えをあらかじめ列挙して、それぞれに対応する画像ファイルを用意しておく必要がある。

46 | 47 | 48 | 49 | 50 | 51 |
サーバサイドプログラム不要
出題のバリエーション1つのみ
解かずにファイルへアクセス総当たりしない限り無理
画像URLの共有可能
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Puzzle Server Example 6 | 7 | 8 | 9 |

Puzzle Server Example

10 | 11 |
12 | 45 | Puzzle Game 46 |
47 |

サーバが出題と検証をする例。

48 |

サーバがIPアドレスと現在時刻を元に出題する問題を決定します。

49 |

サーバは出題時刻と解答を受け取ると、IPアドレスと出題時刻を元に問題を再現し、それに解答を適用して解けるかどうかを判定します。出題より一定時間の間に解けていれば画像ファイルを送信します。

50 |

この方法はセッション管理をしないためサーバ側の手間が少なくて済みますが、IPアドレスをキーにしているので、同じIPアドレスの人同士ならURLを簡単に共有できてしまいます。

51 | 52 | 53 | 54 | 55 | 56 |
サーバサイドプログラム必要
出題のバリエーション多様
解かずにファイルへアクセス総当たりしない限り無理
画像URLの共有IPアドレス、時間により限定
57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/free_mode/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Puzzle Free Mode Example 6 | 7 | 8 | 9 |

Puzzle Free Mode Example

10 | 11 |
12 |
13 | x 14 |
15 |
16 |
17 | 65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /lightsout.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # lightsout.rb 4 | # Copyright (c) 2014 AKIYAMA Kouhei 5 | # This software is released under the MIT License. 6 | 7 | BOARD_W = 7 8 | BOARD_H = 7 9 | TREASURE_CONTENT_TYPE = 'image/jpeg' 10 | TREASURE_FILE_PATH = 'example/common/bg.jpg' 11 | 12 | # ------------------- 13 | BASE64URLCHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' 14 | 15 | require 'cgi' 16 | require 'date' 17 | 18 | class Board 19 | def initialize(w, h) 20 | @w = w 21 | @h = h 22 | @cells = Array.new(BOARD_W*BOARD_H, false) 23 | end 24 | def get_cell(x, y) 25 | @cells[x+y*@w] 26 | end 27 | def flip_on(x, y) 28 | if not (x >= 0 && y >= 0 && x < @w && y < @h) then 29 | return 30 | end 31 | index = x + y * @w 32 | @cells[index] = !@cells[index]; 33 | @cells[index-1] = !@cells[index-1] if x > 0 34 | @cells[index-@w] = !@cells[index-@w] if y > 0 35 | @cells[index+1] = !@cells[index+1] if x+1 < @w 36 | @cells[index+@w] = !@cells[index+@w] if y+1 < @h 37 | end 38 | def is_solved() 39 | not @cells.include?(true); 40 | end 41 | def fill(b) 42 | @cells.fill(b) 43 | end 44 | def randomize(seed) 45 | srand(seed) 46 | fill(false) 47 | begin 48 | for y in 0..@h-1 do 49 | for x in 0..@w-1 do 50 | flip_on(x, y) if rand < 0.5 51 | end 52 | end 53 | end while is_solved() 54 | return self 55 | end 56 | def to_s 57 | r = "" 58 | for y in 0..@h-1 do 59 | for x in 0..@w-1 do 60 | r << if get_cell(x, y) then '#' else '.' end 61 | end 62 | r << ' ' 63 | end 64 | r 65 | end 66 | end 67 | 68 | def get_current_time() 69 | DateTime.now.strftime('%Q').to_i 70 | end 71 | 72 | def get_seed(ip_addr, time) 73 | ip_addr ^ (time & 0xffffffff) ^ ((time>>32)&0xffffffff) 74 | end 75 | 76 | def error(text) 77 | print "Content-Type: text/plain\n\n" 78 | print "Error:" + text + "\n" 79 | end 80 | 81 | def judge(answer, pubtime, ip_addr) 82 | if answer.length > 10000 || pubtime.length > 20 then 83 | error("invalid arguments") 84 | return 85 | end 86 | 87 | pub_time = pubtime.to_i 88 | curr_time = get_current_time 89 | delta_time = curr_time - pub_time 90 | if not (delta_time >= 0 && delta_time < 60*60*1000) 91 | error("out of time") 92 | return 93 | end 94 | 95 | flips = answer.chars.map {|c| 96 | n = BASE64URLCHARS.index(c) || 0 97 | (0..5).map{|i| (n>>i)&1 != 0}}.inject(:+) 98 | if flips.length < BOARD_W*BOARD_H then 99 | error("too short answer") 100 | return 101 | end 102 | 103 | board = Board.new(BOARD_W, BOARD_H).randomize(get_seed(ip_addr, pub_time)) 104 | for y in 0..BOARD_H-1 do 105 | for x in 0..BOARD_W-1 do 106 | board.flip_on(x, y) if flips[x+y*BOARD_W] 107 | end 108 | end 109 | 110 | if board.is_solved then 111 | begin 112 | open(TREASURE_FILE_PATH){|file| 113 | print "Content-Type: " + TREASURE_CONTENT_TYPE + "\n" 114 | print "\n" 115 | $stdout.binmode 116 | $stdout.write(file.read) 117 | } 118 | rescue => e 119 | error(e.to_s) 120 | end 121 | else 122 | error("not solved") 123 | end 124 | end 125 | 126 | def make_problem(ip_addr) 127 | curr_time = get_current_time 128 | 129 | board = Board.new(BOARD_W, BOARD_H).randomize(get_seed(ip_addr, curr_time)) 130 | print "Content-Type: text/plain\n\n" 131 | print curr_time.to_s + "\n" 132 | print board.to_s + "\n" 133 | end 134 | 135 | 136 | cgi = CGI.new 137 | ip_addr = cgi.remote_addr.split('.').inject(0) {|r,v| (r << 8) + v.to_i} 138 | 139 | if cgi.has_key?('a') then 140 | answer = cgi["a"][0] 141 | pubtime = cgi["t"][0] 142 | judge(answer, pubtime, ip_addr) 143 | else 144 | make_problem(ip_addr) 145 | end 146 | -------------------------------------------------------------------------------- /lightsout.js: -------------------------------------------------------------------------------- 1 | // lightsout.js 2 | // Copyright (c) 2014 AKIYAMA Kouhei 3 | // This software is released under the MIT License. 4 | 5 | (function(global){ 6 | if(!global.misohena){ global.misohena = {}; } 7 | if(!global.misohena.js_lightsout){ global.misohena.js_lightsout = {}; } 8 | var mypkg = global.misohena.js_lightsout; 9 | 10 | // 11 | // Model 12 | // 13 | 14 | mypkg.Board = Board; 15 | function Board(w, h, cells, hist){ 16 | var history = hist !== undefined ? hist : new MoveHistory(w, h); 17 | 18 | this.getWidth = function(){return w;}; 19 | this.getHeight = function(){return h;}; 20 | this.getCell = function(x, y){return cells[x+y*w];}; 21 | this.setCell = function(x, y, state){ return cells[x+y*w] = state;}; 22 | this.getNCell = function(i){return cells[i];}; 23 | this.setNCell = function(i, state){ return cells[i] = state;}; 24 | this.fill = function(state){ 25 | for(var i = 0; i < cells.length; ++i){cells[i] = state;} 26 | }; 27 | this.flipOn = function(x, y, suppressRecordHistory){ 28 | if(!(x >= 0 && y >= 0 && x < w && y < h)){ 29 | return; 30 | } 31 | var index = x + y * w; 32 | cells[index] = !cells[index]; 33 | if(x > 0) { 34 | cells[index-1] = !cells[index-1]; 35 | } 36 | if(y > 0) { 37 | cells[index-w] = !cells[index-w]; 38 | } 39 | if(x+1 < w) { 40 | cells[index+1] = !cells[index+1]; 41 | } 42 | if(y+1 < h) { 43 | cells[index+w] = !cells[index+w]; 44 | } 45 | if(history && !suppressRecordHistory){ 46 | history.addMove(x, y); 47 | } 48 | }; 49 | this.isSolved = function(){ 50 | for(var i = 0; i < cells.length; ++i){ 51 | if(cells[i]){ 52 | return false; 53 | } 54 | } 55 | return true; 56 | }; 57 | this.randomize = function(){ 58 | this.fill(false); 59 | do{ 60 | for(var y = 0; y < h; ++y){ 61 | for(var x = 0; x < w; ++x){ 62 | if(Math.random() < 0.5){ 63 | this.flipOn(x, y, true); 64 | } 65 | } 66 | } 67 | } 68 | while(this.isSolved()); 69 | return this; 70 | }; 71 | this.setMoveHistory = function(h){ 72 | return history = h; 73 | }; 74 | this.getMoveHistory = function(){ 75 | return history; 76 | }; 77 | this.clone = function(){ 78 | return new Board(w, h, cells.slice(0), history ? history.clone() : null); 79 | }; 80 | this.setBoard = function(b){ 81 | w = b.getWidth(); 82 | h = b.getHeight(); 83 | cells = new Array(w*h); 84 | for(var i = 0; i < cells.length; ++i){ 85 | cells[i] = b.getNCell(i); 86 | } 87 | var bh = b.getMoveHistory(); 88 | if(bh){ 89 | history = bh.clone(); 90 | } 91 | else{ 92 | history = null; 93 | } 94 | return this; 95 | }; 96 | if(!cells){ 97 | cells = new Array(w*h); 98 | this.fill(false); 99 | } 100 | } 101 | 102 | mypkg.parseBoard = parseBoard; 103 | function parseBoard(text) 104 | { 105 | var MAX_SIZE = 100; 106 | var rows = text.split(/\s+/); 107 | if(!(rows.length > 0 && rows.length <= MAX_SIZE)){ 108 | return null; 109 | } 110 | var w = rows[0].length; 111 | if(!(w > 0 && w <= MAX_SIZE)){ 112 | return null; 113 | } 114 | var cells = []; 115 | var y; 116 | for(y = 0; y < rows.length; ++y){ 117 | if(rows[y].length != w){ 118 | break; 119 | } 120 | for(var x = 0; x < rows[y].length; ++x){ 121 | cells.push(rows[y].charAt(x) == '#'); 122 | } 123 | } 124 | if(y == 0){ 125 | return null; 126 | } 127 | var h = y; 128 | 129 | return new Board(w, h, cells); 130 | } 131 | 132 | // 133 | // Move History 134 | // 135 | function MoveHistory(w, h, movesArg) 136 | { 137 | var cells = new Array(w*h); 138 | var moves = movesArg || []; 139 | this.addMove = addMove; 140 | function addMove(x, y){ 141 | moves.push({x:x, y:y}); 142 | flipCell(x, y); 143 | }; 144 | this.getWidth = function (){ return w;}; 145 | this.getHeight = function (){ return h;}; 146 | this.getMove = function(i){ return moves[i];}; 147 | this.getMoveCount = function(){ return moves.length;}; 148 | 149 | function initCells(){ 150 | for(var i = 0; i < cells.length; ++i){ cells[i] = false;} 151 | } 152 | function flipCell(x, y){ 153 | var index = x+y*w; 154 | cells[index] = !cells[index]; 155 | } 156 | initCells(); 157 | function getCellFliped(x, y){ 158 | return cells[x+y*w]; 159 | } 160 | this.getAnswerText = getAnswerText; 161 | function getAnswerText(){ 162 | return convertBoolArrayToBase64(cells); 163 | } 164 | this.clone = clone; 165 | function clone(){ 166 | return new MoveHistory(w, h, moves.slice(0)); 167 | } 168 | this.setMoveHistory = setMoveHistory; 169 | function setMoveHistory(hist){ 170 | w = hist.getWidth(); 171 | h = hist.getHeight(); 172 | cells = new Array(w*h); 173 | moves = []; 174 | for(var i = 0; i < hist.getMoveCount(); ++i){ 175 | var m = hist.getMove(i); 176 | addMove(m.x, m.y); 177 | } 178 | } 179 | } 180 | var BASE64URL_TBL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; 181 | function toBase64Char(num) 182 | { 183 | return BASE64URL_TBL.charAt(num); 184 | } 185 | function convertBoolArrayToBase64(arr) 186 | { 187 | var text = ""; 188 | var bits = 0; 189 | function flush(){ 190 | text += toBase64Char(bits); 191 | bits = 0; 192 | } 193 | for(var i = 0, bi = 0; i < arr.length; ++i){ 194 | if(arr[i]){ 195 | bits |= (1<= 6){ 198 | bi = 0; 199 | flush(); 200 | } 201 | } 202 | if(bi > 0){ 203 | flush(); 204 | } 205 | return text; 206 | } 207 | function convertIntToBase64(num, bits) 208 | { 209 | var arr = []; 210 | for(var i = 0; i < bits; ++i){ 211 | arr.push( (num & 1) != 0 ); 212 | num >>= 1; 213 | } 214 | return convertBoolArrayToBase64(arr); 215 | } 216 | 217 | // 218 | // Solver 219 | // 220 | mypkg.enumAllSolutionsText = enumAllSolutionsText; 221 | function enumAllSolutionsText(board) 222 | { 223 | var w = board.getWidth(); 224 | var h = board.getHeight(); 225 | var cellCount = w * h; 226 | if(cellCount > 28){ 227 | return null; 228 | } 229 | var solutions = []; 230 | for(var s = 0; s < (1<= 0){ 411 | dots[i].style.opacity = Math.max(0, 1.0 - (tt % CYCLE) / CYCLE); 412 | } 413 | }}, TIMER_INTERVAL); 414 | mark.stop = function(){ 415 | if(intervalId !== undefined){ 416 | clearInterval(intervalId); 417 | delete intervalId; 418 | } 419 | if(mark.parentNode){ 420 | mark.parentNode.removeChild(mark); 421 | } 422 | }; 423 | return mark; 424 | } 425 | 426 | // 427 | // HTML Utility 428 | // 429 | 430 | mypkg.getLastScriptNode = getLastScriptNode; 431 | function getLastScriptNode() 432 | { 433 | var n = document; 434 | while(n && n.nodeName.toLowerCase() != "script") { n = n.lastChild;} 435 | return n; 436 | } 437 | 438 | 439 | mypkg.showImage = showImage; 440 | function showImage(fromTable, imgUrl, linkUrl) 441 | { 442 | var tableWrapperOuter = document.createElement("div"); 443 | tableWrapperOuter.style.padding = "0"; 444 | tableWrapperOuter.style.margin = "0"; 445 | tableWrapperOuter.style.position = "relative"; 446 | fromTable.parentNode.insertBefore(tableWrapperOuter, fromTable); 447 | fromTable.parentNode.removeChild(fromTable); 448 | 449 | var tableWrapperInner = document.createElement("div"); 450 | tableWrapperInner.style.padding = "0"; 451 | tableWrapperInner.style.margin = "0"; 452 | tableWrapperInner.style.display = "inline-block"; 453 | tableWrapperInner.style.position = "relative"; 454 | tableWrapperOuter.appendChild(tableWrapperInner); 455 | tableWrapperInner.appendChild(fromTable); 456 | 457 | var loadingMark = createLoadingMark(); 458 | tableWrapperInner.appendChild(loadingMark); 459 | 460 | var imgLoader = new Image(); 461 | imgLoader.onload = onLoaded; 462 | imgLoader.src = imgUrl; 463 | 464 | function onLoaded() 465 | { 466 | loadingMark.stop(); 467 | 468 | var overlay = document.createElement("div"); 469 | overlay.style.position = "fixed"; 470 | overlay.style.left = "0%"; 471 | overlay.style.top = "0%"; 472 | overlay.style.width = "100%"; 473 | overlay.style.height = "100%"; 474 | overlay.style.background = "rgba(0,0,0,0)"; 475 | overlay.style.zIndex = "99999"; 476 | document.body.appendChild(overlay); 477 | 478 | var img = document.createElement("div"); 479 | img.style.display = "inline-block"; 480 | img.style.position = "relative"; 481 | img.style.backgroundImage = "url('" + imgUrl + "')"; 482 | img.style.backgroundPosition = "center center"; 483 | img.style.backgroundRepeat = "no-repeat"; 484 | img.style.backgroundSize = "contain"; 485 | img.style.cursor = "pointer"; 486 | img.addEventListener("click", function(e){ 487 | location.href = linkUrl || imgUrl; 488 | }, false); 489 | overlay.appendChild(img); 490 | 491 | var fromRect = fromTable.getBoundingClientRect(); 492 | var fromX = fromRect.left + fromRect.width/2; 493 | var fromY = fromRect.top + fromRect.height/2; 494 | 495 | var startTime = Date.now(); 496 | function step(){ 497 | var alpha = Math.min((Date.now() - startTime) / 500, 1.0); 498 | 499 | overlay.style.background = "rgba(0,0,0," + (alpha*0.75).toFixed(3) + ")"; 500 | img.style.opacity = (Math.min(1.0, alpha*4)).toFixed(3); 501 | 502 | function intpl(from, to){ 503 | return (to - from) * alpha + from; 504 | } 505 | img.style.left = intpl(fromX, 0) + "px"; 506 | img.style.top = intpl(fromY, 0) + "px"; 507 | img.style.width = intpl(0, 100) + "%"; 508 | img.style.height = intpl(0, 100) + "%"; 509 | if(alpha >= 1.0){ 510 | } 511 | else{ 512 | setTimeout(step, 10); 513 | } 514 | } 515 | step(); 516 | } 517 | } 518 | 519 | })(this); 520 | --------------------------------------------------------------------------------