binary_puzzle_solver 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Makefile +6 -0
- data/README.md +29 -0
- data/Rakefile +19 -0
- data/binary_puzzle_solver.gemspec +27 -0
- data/lib/binary_puzzle_solver/base.rb +716 -0
- data/lib/binary_puzzle_solver/version.rb +3 -0
- data/lib/binary_puzzle_solver.rb +1 -0
- data/test/parse-board.rb +582 -0
- metadata +90 -0
@@ -0,0 +1,716 @@
|
|
1
|
+
# Copyright (c) 2012 Shlomi Fish
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person
|
4
|
+
# obtaining a copy of this software and associated documentation
|
5
|
+
# files (the "Software"), to deal in the Software without
|
6
|
+
# restriction, including without limitation the rights to use,
|
7
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the
|
9
|
+
# Software is furnished to do so, subject to the following
|
10
|
+
# conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
#
|
24
|
+
# ----------------------------------------------------------------------------
|
25
|
+
#
|
26
|
+
# This is the MIT/X11 Licence. For more information see:
|
27
|
+
#
|
28
|
+
# 1. http://www.opensource.org/licenses/mit-license.php
|
29
|
+
#
|
30
|
+
# 2. http://en.wikipedia.org/wiki/MIT_License
|
31
|
+
|
32
|
+
module Binary_Puzzle_Solver
|
33
|
+
|
34
|
+
class GameIntegrityException < RuntimeError
|
35
|
+
end
|
36
|
+
|
37
|
+
class Coord
|
38
|
+
|
39
|
+
attr_reader :x, :y
|
40
|
+
|
41
|
+
def initialize (params)
|
42
|
+
@x = params[:x]
|
43
|
+
@y = params[:y]
|
44
|
+
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
def rotate
|
49
|
+
return Coord.new(:x=>self.y,:y=>self.x)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Cell
|
54
|
+
UNKNOWN = -1
|
55
|
+
ZERO = 0
|
56
|
+
ONE = 1
|
57
|
+
|
58
|
+
VALID_STATES = {UNKNOWN => true, ZERO => true, ONE => true}
|
59
|
+
|
60
|
+
attr_reader :state
|
61
|
+
|
62
|
+
def initialize (params={})
|
63
|
+
@state = UNKNOWN
|
64
|
+
if params.has_key?('state') then
|
65
|
+
set_state(params[:state])
|
66
|
+
end
|
67
|
+
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_state (new_state)
|
72
|
+
if (not VALID_STATES.has_key?(new_state))
|
73
|
+
raise RuntimeError, "Invalid state " + new_state.to_s;
|
74
|
+
end
|
75
|
+
if (@state != UNKNOWN)
|
76
|
+
raise RuntimeError, "Cannot reassign a value to the already set state."
|
77
|
+
end
|
78
|
+
@state = new_state
|
79
|
+
|
80
|
+
return
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_char()
|
84
|
+
if state == ZERO
|
85
|
+
return '0'
|
86
|
+
elsif state == ONE
|
87
|
+
return '1'
|
88
|
+
else
|
89
|
+
raise RuntimeError, "get_char() called on Unset state"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# A summary for a row or column.
|
95
|
+
class RowSummary
|
96
|
+
attr_reader :limit, :half_limit
|
97
|
+
def initialize (limit)
|
98
|
+
@limit = limit
|
99
|
+
|
100
|
+
if (limit % 2 != 0)
|
101
|
+
raise RuntimeError, "Limit must be even"
|
102
|
+
end
|
103
|
+
|
104
|
+
@half_limit = limit / 2
|
105
|
+
|
106
|
+
@counts = {
|
107
|
+
Cell::ZERO => 0,
|
108
|
+
Cell::ONE => 0,
|
109
|
+
}
|
110
|
+
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_count (value)
|
115
|
+
return @counts[value]
|
116
|
+
end
|
117
|
+
|
118
|
+
def is_full(value)
|
119
|
+
return (get_count(value) == half_limit())
|
120
|
+
end
|
121
|
+
|
122
|
+
def inc_count (value)
|
123
|
+
new_val = (@counts[value] += 1)
|
124
|
+
|
125
|
+
if (new_val > half_limit())
|
126
|
+
raise GameIntegrityException, "Too many #{value}"
|
127
|
+
end
|
128
|
+
|
129
|
+
return
|
130
|
+
end
|
131
|
+
|
132
|
+
def are_both_not_exceeded()
|
133
|
+
return (get_count(Cell::ZERO) <= half_limit() and
|
134
|
+
get_count(Cell::ONE) <= half_limit())
|
135
|
+
end
|
136
|
+
|
137
|
+
def are_both_full()
|
138
|
+
return (is_full(Cell::ZERO) and is_full(Cell::ONE))
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_full_value()
|
142
|
+
if are_both_full()
|
143
|
+
return nil
|
144
|
+
elsif (is_full(Cell::ZERO))
|
145
|
+
return Cell::ZERO
|
146
|
+
elsif (is_full(Cell::ONE))
|
147
|
+
return Cell::ONE
|
148
|
+
else
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
class Move
|
156
|
+
attr_reader :coord, :dir, :reason, :val
|
157
|
+
def initialize (params)
|
158
|
+
@coord = params[:coord]
|
159
|
+
@dir = params[:dir]
|
160
|
+
@reason = params[:reason]
|
161
|
+
@val = params[:val]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Board
|
166
|
+
|
167
|
+
attr_reader :iters_quota, :num_iters_done
|
168
|
+
def initialize (params)
|
169
|
+
@dim_limits = {:x => params[:x], :y => params[:y]}
|
170
|
+
@cells = dim_range(:y).map {
|
171
|
+
dim_range(:x).map{ Cell.new }
|
172
|
+
}
|
173
|
+
@row_summaries = {
|
174
|
+
:x => dim_range(:x).map { RowSummary.new(limit(:y)); },
|
175
|
+
:y => dim_range(:y).map { RowSummary.new(limit(:x)); }
|
176
|
+
}
|
177
|
+
@old_moves = []
|
178
|
+
@new_moves = []
|
179
|
+
|
180
|
+
@iters_quota = 0
|
181
|
+
@num_iters_done = 0
|
182
|
+
|
183
|
+
@state = {:method_idx => 0, :view_idx => 0, :row_idx => 0, };
|
184
|
+
|
185
|
+
return
|
186
|
+
end
|
187
|
+
|
188
|
+
def dim_range(dim)
|
189
|
+
return (0 .. max_idx(dim))
|
190
|
+
end
|
191
|
+
|
192
|
+
def add_to_iters_quota(delta)
|
193
|
+
@iters_quota += delta
|
194
|
+
end
|
195
|
+
|
196
|
+
def rotate_dir(dir)
|
197
|
+
if dir == :x
|
198
|
+
return :y
|
199
|
+
else
|
200
|
+
return :x
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def num_moves_done()
|
205
|
+
return @new_moves.length
|
206
|
+
end
|
207
|
+
|
208
|
+
def flush_moves()
|
209
|
+
@old_moves += @new_moves
|
210
|
+
@new_moves = []
|
211
|
+
|
212
|
+
return
|
213
|
+
end
|
214
|
+
|
215
|
+
def add_move(m)
|
216
|
+
@new_moves.push(m)
|
217
|
+
|
218
|
+
return
|
219
|
+
end
|
220
|
+
|
221
|
+
def get_new_move(idx)
|
222
|
+
return @new_moves[idx]
|
223
|
+
end
|
224
|
+
|
225
|
+
def limit(dim)
|
226
|
+
return @dim_limits[dim]
|
227
|
+
end
|
228
|
+
|
229
|
+
def max_idx(dim)
|
230
|
+
return limit(dim) - 1
|
231
|
+
end
|
232
|
+
|
233
|
+
def _get_cell(coord)
|
234
|
+
|
235
|
+
x = coord.x
|
236
|
+
y = coord.y
|
237
|
+
|
238
|
+
if (y < 0)
|
239
|
+
raise RuntimeError, "y cannot be lower than 0."
|
240
|
+
end
|
241
|
+
|
242
|
+
if (x < 0)
|
243
|
+
raise RuntimeError, "x cannot be lower than 0."
|
244
|
+
end
|
245
|
+
|
246
|
+
if (y > max_idx(:y))
|
247
|
+
raise RuntimeError, "y cannot be higher than max_idx."
|
248
|
+
end
|
249
|
+
|
250
|
+
if (x > max_idx(:x))
|
251
|
+
raise RuntimeError, "x cannot be higher than max_idx."
|
252
|
+
end
|
253
|
+
|
254
|
+
return @cells[y][x]
|
255
|
+
end
|
256
|
+
|
257
|
+
def set_cell_state(coord, state)
|
258
|
+
_get_cell(coord).set_state(state)
|
259
|
+
get_row_summary(:dim => :x, :idx => coord.x).inc_count(state)
|
260
|
+
get_row_summary(:dim => :y, :idx => coord.y).inc_count(state)
|
261
|
+
return
|
262
|
+
end
|
263
|
+
|
264
|
+
def get_cell_state(coord)
|
265
|
+
return _get_cell(coord).state
|
266
|
+
end
|
267
|
+
|
268
|
+
# There is an equivalence between the dimensions, so
|
269
|
+
# a view allows us to view the board rotated.
|
270
|
+
def get_view(params)
|
271
|
+
return Board_View.new(self, params[:rotate])
|
272
|
+
end
|
273
|
+
|
274
|
+
def get_row_summary(params)
|
275
|
+
idx = params[:idx]
|
276
|
+
dim = params[:dim]
|
277
|
+
|
278
|
+
if (idx < 0)
|
279
|
+
raise RuntimeError, "idx cannot be lower than 0."
|
280
|
+
end
|
281
|
+
if (idx > max_idx(dim))
|
282
|
+
raise RuntimeError, "idx cannot be higher than max_idx."
|
283
|
+
end
|
284
|
+
return @row_summaries[dim][idx]
|
285
|
+
end
|
286
|
+
|
287
|
+
def opposite_value(val)
|
288
|
+
if (val == Cell::ZERO)
|
289
|
+
return Cell::ONE
|
290
|
+
elsif (val == Cell::ONE)
|
291
|
+
return Cell::ZERO
|
292
|
+
else
|
293
|
+
raise RuntimeError, "'#{val}' must be zero or one."
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def try_to_solve_using (params)
|
298
|
+
methods_list = params[:methods]
|
299
|
+
views = [get_view(:rotate => false), get_view(:rotate => true), ]
|
300
|
+
|
301
|
+
catch :out_of_iters do
|
302
|
+
first_iter = true
|
303
|
+
|
304
|
+
while first_iter or num_moves_done() > 0
|
305
|
+
first_iter = false
|
306
|
+
flush_moves()
|
307
|
+
while (@state[:method_idx] < methods_list.length)
|
308
|
+
m = methods_list[@state[:method_idx]]
|
309
|
+
while (@state[:view_idx] < views.length)
|
310
|
+
v = views[@state[:view_idx]]
|
311
|
+
while (@state[:row_idx] <= v.max_idx(v.row_dim()))
|
312
|
+
row_idx = @state[:row_idx]
|
313
|
+
@state[:row_idx] += 1
|
314
|
+
v.method(m).call(:idx => row_idx)
|
315
|
+
@iters_quota -= 1
|
316
|
+
@num_iters_done += 1
|
317
|
+
if iters_quota == 0
|
318
|
+
throw :out_of_iters
|
319
|
+
end
|
320
|
+
end
|
321
|
+
@state[:view_idx] += 1
|
322
|
+
@state[:row_idx] = 0
|
323
|
+
end
|
324
|
+
@state[:method_idx] += 1
|
325
|
+
@state[:view_idx] = 0
|
326
|
+
end
|
327
|
+
@state[:method_idx] = 0
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
return
|
332
|
+
end
|
333
|
+
|
334
|
+
def as_string()
|
335
|
+
return dim_range(:y).map { |y|
|
336
|
+
['|'] +
|
337
|
+
dim_range(:x).map { |x|
|
338
|
+
s = get_cell_state(
|
339
|
+
Coord.new(:y => y, :x => x)
|
340
|
+
)
|
341
|
+
((s == Cell::UNKNOWN) ? ' ' : s.to_s())
|
342
|
+
} +
|
343
|
+
["|\n"]
|
344
|
+
}.inject([]) { |a,e| a+e }.join('')
|
345
|
+
end
|
346
|
+
|
347
|
+
def validate()
|
348
|
+
views = [get_view(:rotate => false), get_view(:rotate => true), ]
|
349
|
+
|
350
|
+
is_final = true
|
351
|
+
|
352
|
+
views.each do |v|
|
353
|
+
view_final = v.validate_rows()
|
354
|
+
is_final &&= view_final
|
355
|
+
end
|
356
|
+
|
357
|
+
if is_final
|
358
|
+
return :final
|
359
|
+
else
|
360
|
+
return :non_final
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
class Board_View < Board
|
367
|
+
def initialize (board, rotation)
|
368
|
+
@board = board
|
369
|
+
@rotation = rotation
|
370
|
+
if rotation
|
371
|
+
@dims_map = {:x => :y, :y => :x}
|
372
|
+
else
|
373
|
+
@dims_map = {:x => :x, :y => :y}
|
374
|
+
end
|
375
|
+
|
376
|
+
return
|
377
|
+
end
|
378
|
+
|
379
|
+
def rotate_coord(coord)
|
380
|
+
return coord.rotate
|
381
|
+
end
|
382
|
+
|
383
|
+
def _calc_mapped_item(item, rotation_method)
|
384
|
+
if @rotation then
|
385
|
+
return method(rotation_method).call(item)
|
386
|
+
else
|
387
|
+
return item
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def _calc_mapped_coord(coord)
|
392
|
+
return _calc_mapped_item(coord, :rotate_coord)
|
393
|
+
end
|
394
|
+
|
395
|
+
def _calc_mapped_dir(dir)
|
396
|
+
return _calc_mapped_item(dir, :rotate_dir)
|
397
|
+
end
|
398
|
+
|
399
|
+
def _get_cell(coord)
|
400
|
+
return @board._get_cell(_calc_mapped_coord(coord))
|
401
|
+
end
|
402
|
+
|
403
|
+
def limit(dim)
|
404
|
+
return @board.limit(@dims_map[dim])
|
405
|
+
end
|
406
|
+
|
407
|
+
def get_row_summary(params)
|
408
|
+
return @board.get_row_summary(
|
409
|
+
:idx => params[:idx],
|
410
|
+
:dim => @dims_map[params[:dim]]
|
411
|
+
)
|
412
|
+
end
|
413
|
+
|
414
|
+
def row_dim()
|
415
|
+
return :y
|
416
|
+
end
|
417
|
+
|
418
|
+
def col_dim()
|
419
|
+
return :x
|
420
|
+
end
|
421
|
+
|
422
|
+
def get_row_handle(idx)
|
423
|
+
return RowHandle.new(self, idx)
|
424
|
+
end
|
425
|
+
|
426
|
+
def _append_move(params)
|
427
|
+
coord = params[:coord]
|
428
|
+
dir = params[:dir]
|
429
|
+
|
430
|
+
@board.add_move(
|
431
|
+
Move.new(
|
432
|
+
# TODO : Extract a function for the coord rotation.
|
433
|
+
:coord => _calc_mapped_coord(coord),
|
434
|
+
:val => params[:val],
|
435
|
+
:reason => params[:reason],
|
436
|
+
:dir => _calc_mapped_dir(dir)
|
437
|
+
)
|
438
|
+
)
|
439
|
+
|
440
|
+
return
|
441
|
+
end
|
442
|
+
|
443
|
+
def perform_and_append_move(params)
|
444
|
+
set_cell_state(params[:coord], params[:val])
|
445
|
+
_append_move(params)
|
446
|
+
end
|
447
|
+
|
448
|
+
def check_and_handle_sequences_in_row(params)
|
449
|
+
row_idx = params[:idx]
|
450
|
+
|
451
|
+
row = get_row_handle(row_idx)
|
452
|
+
|
453
|
+
prev_cell_states = []
|
454
|
+
|
455
|
+
max_in_a_row = 2
|
456
|
+
|
457
|
+
handle_prev_cell_states = lambda { |x|
|
458
|
+
if prev_cell_states.length > max_in_a_row then
|
459
|
+
raise GameIntegrityException, "Too many #{prev_cell_states[0]} in a row"
|
460
|
+
elsif (prev_cell_states.length == max_in_a_row)
|
461
|
+
coords = Array.new
|
462
|
+
start_x = x - max_in_a_row - 1;
|
463
|
+
if (start_x >= 0)
|
464
|
+
coords << row.get_coord(start_x)
|
465
|
+
end
|
466
|
+
if (x <= row.max_idx())
|
467
|
+
coords << row.get_coord(x)
|
468
|
+
end
|
469
|
+
coords.each do |c|
|
470
|
+
if (get_cell_state(c) == Cell::UNKNOWN)
|
471
|
+
perform_and_append_move(
|
472
|
+
:coord => c,
|
473
|
+
:val => opposite_value(prev_cell_states[0]),
|
474
|
+
:reason => "Vicinity to two in a row",
|
475
|
+
:dir => col_dim()
|
476
|
+
)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
return
|
482
|
+
}
|
483
|
+
|
484
|
+
row.iter().each do |x, cell|
|
485
|
+
cell_state = cell.state
|
486
|
+
|
487
|
+
if cell_state == Cell::UNKNOWN
|
488
|
+
handle_prev_cell_states.call(x)
|
489
|
+
prev_cell_states = []
|
490
|
+
elsif ((prev_cell_states.length == 0) or
|
491
|
+
(prev_cell_states[-1] != cell_state)) then
|
492
|
+
handle_prev_cell_states.call(x)
|
493
|
+
prev_cell_states = [cell_state]
|
494
|
+
else
|
495
|
+
prev_cell_states << cell_state
|
496
|
+
end
|
497
|
+
end
|
498
|
+
handle_prev_cell_states.call(row.max_idx + 1)
|
499
|
+
|
500
|
+
return
|
501
|
+
end
|
502
|
+
|
503
|
+
def check_and_handle_known_unknown_sameknown_in_row(params)
|
504
|
+
row_idx = params[:idx]
|
505
|
+
|
506
|
+
prev_cell_states = []
|
507
|
+
|
508
|
+
max_in_a_row = 2
|
509
|
+
|
510
|
+
row = get_row_handle(row_idx)
|
511
|
+
|
512
|
+
(1 .. (row.max_idx - 1)).each do |x|
|
513
|
+
|
514
|
+
get_coord = lambda { |offset| row.get_coord(x+offset); }
|
515
|
+
get_state = lambda { |offset| row.get_state(x+offset); }
|
516
|
+
|
517
|
+
if (get_state.call(-1) != Cell::UNKNOWN and
|
518
|
+
get_state.call(0) == Cell::UNKNOWN and
|
519
|
+
get_state.call(1) == get_state.call(-1))
|
520
|
+
then
|
521
|
+
|
522
|
+
perform_and_append_move(
|
523
|
+
:coord => get_coord.call(0),
|
524
|
+
:val => opposite_value(get_state.call(-1)),
|
525
|
+
:reason => "In between two identical cells",
|
526
|
+
:dir => col_dim()
|
527
|
+
)
|
528
|
+
end
|
529
|
+
|
530
|
+
end
|
531
|
+
|
532
|
+
return
|
533
|
+
end
|
534
|
+
|
535
|
+
def check_and_handle_cells_of_one_value_in_row_were_all_found(params)
|
536
|
+
row_idx = params[:idx]
|
537
|
+
|
538
|
+
row = get_row_handle(row_idx)
|
539
|
+
|
540
|
+
full_val = row.get_summary().find_full_value()
|
541
|
+
|
542
|
+
if (full_val)
|
543
|
+
opposite_val = opposite_value(full_val)
|
544
|
+
|
545
|
+
row.iter().each do |x, cell|
|
546
|
+
cell_state = cell.state
|
547
|
+
|
548
|
+
if cell_state == Cell::UNKNOWN
|
549
|
+
perform_and_append_move(
|
550
|
+
:coord => row.get_coord(x),
|
551
|
+
:val => opposite_val,
|
552
|
+
:reason => "Filling unknowns in row with an exceeded value with the other value",
|
553
|
+
:dir => col_dim()
|
554
|
+
)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
return
|
560
|
+
end
|
561
|
+
|
562
|
+
def validate_rows()
|
563
|
+
# TODO
|
564
|
+
complete_rows_map = Hash.new
|
565
|
+
|
566
|
+
is_final = true
|
567
|
+
|
568
|
+
dim_range(row_dim()).each do |row_idx|
|
569
|
+
row = get_row_handle(row_idx)
|
570
|
+
ret = row.validate( :complete_rows_map => complete_rows_map )
|
571
|
+
is_final &&= ret[:is_final]
|
572
|
+
end
|
573
|
+
|
574
|
+
return is_final
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
class RowHandle
|
579
|
+
attr_reader :view, :idx
|
580
|
+
def initialize (init_view, init_idx)
|
581
|
+
@view = init_view
|
582
|
+
@idx = init_idx
|
583
|
+
end
|
584
|
+
|
585
|
+
def get_summary()
|
586
|
+
return view.get_row_summary(:idx => idx, :dim => row_dim());
|
587
|
+
end
|
588
|
+
|
589
|
+
def get_string()
|
590
|
+
return iter().map { |x, cell| cell.get_char() }.join('')
|
591
|
+
end
|
592
|
+
|
593
|
+
def col_dim()
|
594
|
+
return view.col_dim()
|
595
|
+
end
|
596
|
+
|
597
|
+
def row_dim()
|
598
|
+
return view.row_dim()
|
599
|
+
end
|
600
|
+
|
601
|
+
def max_idx()
|
602
|
+
return view.max_idx(view.col_dim())
|
603
|
+
end
|
604
|
+
|
605
|
+
def get_coord(x)
|
606
|
+
return Coord.new(col_dim() => x, row_dim() => idx)
|
607
|
+
end
|
608
|
+
|
609
|
+
def get_state(x)
|
610
|
+
return view.get_cell_state(get_coord(x))
|
611
|
+
end
|
612
|
+
|
613
|
+
def iter
|
614
|
+
return view.dim_range(col_dim()).map { |x|
|
615
|
+
[x, view._get_cell(get_coord(x))]
|
616
|
+
}
|
617
|
+
end
|
618
|
+
|
619
|
+
def check_for_duplicated(complete_rows_map)
|
620
|
+
summary = get_summary()
|
621
|
+
|
622
|
+
if not summary.are_both_not_exceeded() then
|
623
|
+
raise GameIntegrityException, "Value exceeded"
|
624
|
+
elsif summary.are_both_full() then
|
625
|
+
s = get_string()
|
626
|
+
complete_rows_map[s] ||= []
|
627
|
+
dups = complete_rows_map[s]
|
628
|
+
dups << idx
|
629
|
+
if (dups.length > 1)
|
630
|
+
i, j = dups[0], dups[1]
|
631
|
+
raise GameIntegrityException, \
|
632
|
+
"Duplicate Rows - #{i} and #{j}"
|
633
|
+
end
|
634
|
+
return true
|
635
|
+
else
|
636
|
+
return false
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
|
641
|
+
def check_for_too_many_consecutive()
|
642
|
+
count = 0
|
643
|
+
prev_cell_state = Cell::UNKNOWN
|
644
|
+
|
645
|
+
handle_seq = lambda {
|
646
|
+
if ((prev_cell_state == Cell::ZERO) || \
|
647
|
+
(prev_cell_state == Cell::ONE)) then
|
648
|
+
if count > 2 then
|
649
|
+
raise GameIntegrityException, \
|
650
|
+
"Too many #{prev_cell_state} in a row"
|
651
|
+
end
|
652
|
+
end
|
653
|
+
}
|
654
|
+
|
655
|
+
iter().each do |x, cell|
|
656
|
+
cell_state = cell.state
|
657
|
+
if cell_state == prev_cell_state then
|
658
|
+
count += 1
|
659
|
+
else
|
660
|
+
handle_seq.call()
|
661
|
+
count = 1
|
662
|
+
prev_cell_state = cell_state
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
handle_seq.call()
|
667
|
+
|
668
|
+
return
|
669
|
+
end
|
670
|
+
|
671
|
+
def validate(params)
|
672
|
+
complete_rows_map = params[:complete_rows_map]
|
673
|
+
|
674
|
+
check_for_too_many_consecutive()
|
675
|
+
|
676
|
+
return { :is_final => check_for_duplicated(complete_rows_map), };
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def Binary_Puzzle_Solver.gen_board_from_string_v1(string)
|
681
|
+
lines = string.lines.map { |l| l.chomp }
|
682
|
+
line_lens = lines.map { |l| l.length }
|
683
|
+
min_line_len = line_lens.min
|
684
|
+
max_line_len = line_lens.max
|
685
|
+
if (min_line_len != max_line_len)
|
686
|
+
raise RuntimeError, "lines are not uniform in length"
|
687
|
+
end
|
688
|
+
width = min_line_len - 2
|
689
|
+
height = lines.length
|
690
|
+
|
691
|
+
board = Board.new(:x => width, :y => height)
|
692
|
+
|
693
|
+
board.dim_range(:y).each do |y|
|
694
|
+
l = lines[y]
|
695
|
+
if not l =~ /^\|[01 ]+\|$/
|
696
|
+
raise RuntimeError, "Invalid format for line #{y+1}"
|
697
|
+
end
|
698
|
+
board.dim_range(:x).each do |x|
|
699
|
+
c = l[x+1,1]
|
700
|
+
state = false
|
701
|
+
if (c == '1')
|
702
|
+
state = Cell::ONE
|
703
|
+
elsif (c == '0')
|
704
|
+
state = Cell::ZERO
|
705
|
+
end
|
706
|
+
|
707
|
+
if state
|
708
|
+
board.set_cell_state(Coord.new(:x => x, :y => y), state)
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
return board
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'binary_puzzle_solver/base'
|