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.
@@ -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,3 @@
1
+ module Binary_Puzzle_Solver
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require 'binary_puzzle_solver/base'