├── 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 | | 画像URLの共有 | 可能 |
38 |
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 | | 出題のバリエーション | 1つのみ |
49 | | 解かずにファイルへアクセス | 総当たりしない限り無理 |
50 | | 画像URLの共有 | 可能 |
51 |
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 | | 画像URLの共有 | IPアドレス、時間により限定 |
56 |
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 |
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 |
--------------------------------------------------------------------------------