cliptic 0.1.0

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,64 @@
1
+ module Cliptic
2
+ class Pos
3
+ def self.mk(y,x)
4
+ { y:y.to_i, x:x.to_i }
5
+ end
6
+ def self.wrap(val:, min:, max:)
7
+ val > max ? min : (val < min ? max : val)
8
+ end
9
+ def self.change_dir(dir)
10
+ dir == :a ? :d : :a
11
+ end
12
+ end
13
+ class Date < Date
14
+ def to_long
15
+ self.strftime('%A %b %-d %Y')
16
+ end
17
+ end
18
+ class Time < Time
19
+ def self.abs(t)
20
+ Time.at(t).utc
21
+ end
22
+ def to_s
23
+ self.strftime("%T")
24
+ end
25
+ end
26
+ module Chars
27
+ HL = "\u2501"
28
+ LL = "\u2517"
29
+ LU = "\u250F"
30
+ RL = "\u251B"
31
+ RU = "\u2513"
32
+ TD = "\u2533"
33
+ TL = "\u252B"
34
+ TR = "\u2523"
35
+ TU = "\u253B"
36
+ VL = "\u2503"
37
+ XX = "\u254B"
38
+ Tick = "\u2713"
39
+ Nums = ["\u2080", "\u2081", "\u2082", "\u2083",
40
+ "\u2084", "\u2085", "\u2086", "\u2087",
41
+ "\u2088", "\u2089"]
42
+ MS = "\u2588"
43
+ LS = "\u258C"
44
+ RS = "\u2590"
45
+ Block = RS+MS+LS
46
+ def self.small_num(n)
47
+ n.to_s.chars.map{|n| Nums[n.to_i]}.join
48
+ end
49
+ end
50
+ module Errors
51
+ class Invalid_Date < StandardError
52
+ attr_reader :date
53
+ def initialize(date)
54
+ @date = date
55
+ end
56
+ def message
57
+ <<~msg
58
+ Invalid date passed #{date}
59
+ Earliest date #{Date.today<<9}
60
+ msg
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,839 @@
1
+ module Cliptic
2
+ module Main
3
+ module Windows
4
+ class Top_Bar < Cliptic::Interface::Top_Bar
5
+ def initialize(date:Date.today)
6
+ super(date:date)
7
+ end
8
+ end
9
+ class Bottom_Bar < Cliptic::Interface::Bottom_Bar
10
+ def draw
11
+ super
12
+ add_str(x:-1, str:controls)
13
+ end
14
+ def mode(mode)
15
+ setpos.color($colors[mode]) << mode_str(mode)
16
+ .center(8)
17
+ color.refresh
18
+ end
19
+ def unsaved(bool)
20
+ add_str(x:9, str:(bool ? "| +" : " "), bold:bool)
21
+ end
22
+ private
23
+ def mode_str(mode)
24
+ { N:"NORMAL", I:"INSERT" }[mode]
25
+ end
26
+ def controls
27
+ "^S save | ^R reveal | ^E reset | ^G check"
28
+ end
29
+ end
30
+ class Grid < Cliptic::Windows::Grid
31
+ attr_reader :indices, :blocks
32
+ def initialize(puzzle:)
33
+ super(**puzzle.size, line:1)
34
+ @indices,@blocks = puzzle.indices,puzzle.blocks
35
+ link_cells_to_clues(clues:puzzle.clues)
36
+ end
37
+ def draw
38
+ super
39
+ add_indices
40
+ add_blocks
41
+ end
42
+ def cell(y:, x:)
43
+ cells[y][x]
44
+ end
45
+ private
46
+ def add_indices
47
+ indices.each{|i,pos|cell(**pos).set_number(n:i)}
48
+ end
49
+ def add_blocks
50
+ blocks.each{|pos| cell(**pos).set_block}
51
+ end
52
+ def make_cells(y:, x:)
53
+ y.times.map{|iy| x.times.map{|ix|
54
+ Cell.new(sq:Pos.mk(iy,ix), grid:self) }}
55
+ end
56
+ def link_cells_to_clues(clues:)
57
+ clues.each do |clue|
58
+ clue.cells=clue.coords.map{|pos|cell(**pos)}
59
+ end
60
+ end
61
+ end
62
+ class Cell < Cliptic::Windows::Cell
63
+ attr_reader :index, :blocked, :buffer
64
+ attr_accessor :locked
65
+ def initialize(sq:, grid:)
66
+ super(sq:sq, grid:grid)
67
+ @index, @blocked, @locked = false, false, false
68
+ @buffer = " "
69
+ end
70
+ def set_number(n:index, active:false)
71
+ @index = n unless index
72
+ grid.color(active ?
73
+ $colors[:active_num] :
74
+ $colors[:num])
75
+ focus(y:-1, x:-1)
76
+ grid << Chars.small_num(n)
77
+ grid.color
78
+ end
79
+ def set_block
80
+ focus(x:-1).grid.color($colors[:block]) << Chars::Block
81
+ @blocked = true
82
+ grid.color
83
+ end
84
+ def underline
85
+ grid.attron(Curses::A_UNDERLINE)
86
+ write
87
+ grid.attroff(Curses::A_UNDERLINE)
88
+ end
89
+ def write(char=@buffer)
90
+ unless @locked
91
+ super(char)
92
+ @buffer = char
93
+ end; self
94
+ end
95
+ def unlock
96
+ @locked = false unless @blocked; self
97
+ end
98
+ def color(cp)
99
+ grid.color(cp)
100
+ write
101
+ grid.color
102
+ end
103
+ def clear
104
+ @locked, @blocked = false, false
105
+ @buffer = " "
106
+ end
107
+ end
108
+ class Cluebox < Cliptic::Windows::Window
109
+ def initialize(grid:)
110
+ super(y:Curses.lines-grid.y-2, line:grid.y+1, col:0)
111
+ end
112
+ def show(clue:)
113
+ draw(cp:$colors[:cluebox])
114
+ set_meta(clue)
115
+ set_hint(clue)
116
+ noutrefresh
117
+ end
118
+ private
119
+ def set_meta(clue)
120
+ add_str(y:0, x:2, str:clue.meta,
121
+ cp:clue.done ?
122
+ $colors[:correct] : $colors[:meta])
123
+ end
124
+ def set_hint(clue)
125
+ wrap_str(str:clue.hint, line:1)
126
+ end
127
+ def bottom_border
128
+ side_border
129
+ end
130
+ end
131
+ end
132
+ module Fetch
133
+ class Request
134
+ URL="https://data.puzzlexperts.com/puzzleapp-v3/data.php"
135
+ attr_reader :data
136
+ def initialize(date:Date.today, psid:100000160)
137
+ @data = {date:date, psid:psid}
138
+ end
139
+ def send_request
140
+ valid_input? ? raw : (raise Cliptic::Errors::Invalid_Date.new(data[:date]))
141
+ end
142
+ def valid_input?
143
+ JSON.parse(raw, symbolize_names:true)
144
+ .dig(:cells, 0, :meta, :data).length > 0
145
+ end
146
+ def raw
147
+ @raw || Curl.get(URL, data).body
148
+ end
149
+ end
150
+ class Cache < Request
151
+ Path = "#{Dir.home}/.cache/cliptic"
152
+ def initialize(date:Date.today)
153
+ super(date:date)
154
+ make_cache_dir
155
+ end
156
+ def query
157
+ date_cached? ? read_cache : send_request
158
+ .tap{|str| write_cache(str)}
159
+ end
160
+ def make_cache_dir
161
+ FileUtils.mkdir_p(Path) unless Dir.exist?(Path)
162
+ end
163
+ def date_cached?
164
+ File.exist?(file_path)
165
+ end
166
+ def file_path
167
+ "#{Path}/#{data[:date]}"
168
+ end
169
+ def read_cache
170
+ File.read(file_path)
171
+ end
172
+ def write_cache(str)
173
+ File.write(file_path, str)
174
+ end
175
+ end
176
+ class Parser
177
+ attr_reader :raw
178
+ def initialize(date:Date.today)
179
+ @raw = Cache.new(date:date).query
180
+ end
181
+ def parse
182
+ [ parse_clues(raw), parse_size(raw) ]
183
+ end
184
+ private
185
+ def parse_size(raw)
186
+ Pos.mk(*["rows", "columns"]
187
+ .map{|field| raw.scan(/#{field}=(.*?(?=&))/)[0][0]}
188
+ )
189
+ end
190
+ def parse_clues(raw)
191
+ JSON.parse(raw, symbolize_names:true)
192
+ .dig(:cells, 0, :meta, :data)
193
+ .gsub(/^(.*?&){3}(.*)&id=.*$/, "\\2")
194
+ .split(/(?:^|&).*?=/).drop(1)
195
+ .each_slice(5).to_a
196
+ .map{|data| Puzzle::Clue.new(**struct_clue(data))}
197
+ end
198
+ def struct_clue(raw_clue)
199
+ {
200
+ ans:raw_clue[0].chars.map(&:upcase),
201
+ hint:CGI.unescape(raw_clue[1]),
202
+ dir:raw_clue[2].to_sym,
203
+ start:Pos.mk(raw_clue[3], raw_clue[4])
204
+ }
205
+ end
206
+ end
207
+ end
208
+ module Puzzle
209
+ class Puzzle
210
+ include Fetch
211
+ attr_reader :clues, :size, :indices, :map,
212
+ :sorted, :blocks
213
+ def initialize(date:Date.today)
214
+ @clues, @size = Parser.new(date:date).parse
215
+ @indices = index_clues
216
+ @map = map_clues
217
+ @sorted = order_clues
218
+ @blocks = find_blocks
219
+ chain_clues
220
+ end
221
+ def first_clue
222
+ sorted[:a][0].index == 1 ?
223
+ sorted[:a][0] :
224
+ sorted[:d][0]
225
+ end
226
+ def get_clue(y:, x:, dir:)
227
+ map[:index][dir][y][x].is_a?(Clue) ?
228
+ map[:index][dir][y][x] :
229
+ map[:index][Pos.change_dir(dir)][y][x]
230
+ end
231
+ def get_clue_by_index(i:, dir:)
232
+ sorted[dir]
233
+ .find{|clue| clue.index == i} ||
234
+ sorted[Pos.change_dir(dir)]
235
+ .find{|clue| clue.index == i}
236
+ end
237
+ def complete?
238
+ clues.all?{|c| c.done}
239
+ end
240
+ def n_clues_done
241
+ clues.select{|c| c.done}.count
242
+ end
243
+ def n_clues
244
+ clues.count
245
+ end
246
+ def check_all
247
+ clues.each{|c| c.check}
248
+ end
249
+ private
250
+ def index_clues
251
+ clues.map{|clue| clue.start.values}.uniq.sort
252
+ .each_with_index
253
+ .map{ |pos, n| [n+1, Pos.mk(*pos)] }.to_h
254
+ .each{|n, pos| clues.find_all{|clue| clue.start==pos}
255
+ .each{|clue| clue.index = n}}
256
+ end
257
+ def empty
258
+ Array.new(size[:y]){ Array.new(size[:x], ".") }
259
+ end
260
+ def map_clues
261
+ { index:{a:empty, d:empty}, chars:empty }.tap do |map|
262
+ clues.each do |clue|
263
+ clue.coords.zip(clue.ans) do |pos, char|
264
+ map[:index][clue.dir][pos[:y]][pos[:x]] = clue
265
+ map[:chars][pos[:y]][pos[:x]] = char
266
+ end
267
+ end
268
+ end
269
+ end
270
+ def order_clues
271
+ {a:[], d:[]}.tap do |order|
272
+ clues.map{|clue| order[clue.dir] << clue}
273
+ end
274
+ end
275
+ def find_blocks
276
+ [].tap do |a|
277
+ map[:chars].each_with_index.map do |row, y|
278
+ row.each_with_index.map do |char, x|
279
+ a << Pos.mk(y,x) if char == "."
280
+ end
281
+ end
282
+ end
283
+ end
284
+ def chain_clues
285
+ sorted.each do |dir, clues|
286
+ clues.each_with_index do |clue, i|
287
+ clue.next = sorted[dir][i+1] ||
288
+ sorted[Pos.change_dir(dir)][0]
289
+ clue.prev = i == 0 ?
290
+ sorted[Pos.change_dir(dir)].last :
291
+ sorted[dir][i-1]
292
+ end
293
+ end
294
+ end
295
+ end
296
+ class Clue
297
+ attr_reader :ans, :dir, :start, :hint, :length,
298
+ :coords
299
+ attr_accessor :done, :index, :next, :prev, :cells
300
+ def initialize(ans:, hint:, dir:, start:)
301
+ @ans, @dir, @start = ans, dir, start
302
+ @length = ans.length
303
+ @hint = parse_hint(hint)
304
+ @coords = map_coords(**start, l:length)
305
+ @done = false
306
+ end
307
+ def meta
308
+ @meta || "#{index} #{dir==:a ? "across" : "down"}"
309
+ end
310
+ def activate
311
+ cells.first.set_number(active:true)
312
+ cells.each{|c| c.underline}
313
+ end
314
+ def deactivate
315
+ cells.first.set_number(active:false)
316
+ cells.each{|c| c.write}
317
+ check if $config[:auto_mark]
318
+ end
319
+ def has_cell?(y:, x:)
320
+ coords.include?(Pos.mk(y,x))
321
+ end
322
+ def check
323
+ if full?
324
+ correct? ? mark_correct : mark_incorrect
325
+ end
326
+ end
327
+ def full?
328
+ get_buffer.reject{|b| b == " "}.count == length
329
+ end
330
+ def clear
331
+ cells.each{|c| c.write(" ")}
332
+ end
333
+ def reveal
334
+ ans.zip(cells){|char, cell| cell.write(char)}
335
+ mark_correct
336
+ end
337
+ private
338
+ def parse_hint(hint)
339
+ hint.match?(/^.*\(.*\)$/) ?
340
+ hint : "#{hint} (#{length})"
341
+ end
342
+ def map_coords(y:, x:, l:)
343
+ case dir
344
+ when :a then x.upto(x+l-1)
345
+ .map{|ix| Pos.mk(y,ix)}
346
+ when :d then y.upto(y+l-1)
347
+ .map{|iy| Pos.mk(iy,x)}
348
+ end
349
+ end
350
+ def get_buffer
351
+ cells.map{|c| c.buffer}
352
+ end
353
+ def correct?
354
+ get_buffer.join == ans.join
355
+ end
356
+ def mark_correct
357
+ cells.each do |cell|
358
+ cell.color($colors[:correct])
359
+ cell.locked = true
360
+ end
361
+ @done = true
362
+ end
363
+ def mark_incorrect
364
+ cells.each{|c| c.color($colors[:incorrect])}
365
+ end
366
+ end
367
+ end
368
+ module Player
369
+ module Menus
370
+ class Pause < Interface::Menu
371
+ attr_reader :game
372
+ def initialize(game:)
373
+ super
374
+ @game = game
375
+ @draw_bars = false
376
+ end
377
+ def opts
378
+ {
379
+ "Continue" => ->{back; game.unpause},
380
+ "Exit Game" => ->{back; game.exit}
381
+ }
382
+ end
383
+ def title
384
+ "Paused"
385
+ end
386
+ def ctrls
387
+ super.merge({
388
+ ?q => ->{back; game.unpause}
389
+ })
390
+ end
391
+ end
392
+ class Puzzle_Complete < Interface::Menu
393
+ def initialize
394
+ super
395
+ @draw_bars = false
396
+ end
397
+ def opts
398
+ {
399
+ "Exit" => ->{back; Screen.clear},
400
+ "Quit" => ->{exit}
401
+ }
402
+ end
403
+ def title
404
+ "Puzzle Complete!"
405
+ end
406
+ def ctrls
407
+ super.merge({
408
+ ?q => ->{back; Screen.clear}
409
+ })
410
+ end
411
+ end
412
+ class Reset_Progress < Interface::Yes_No_Menu
413
+ def initialize(game:)
414
+ super(yes:->{game.reset},
415
+ post_proc:->{game.unpause})
416
+ @draw_bars = false
417
+ end
418
+ def title
419
+ "Reset puzzle progress?"
420
+ end
421
+ end
422
+ end
423
+ class Board
424
+ include Puzzle, Windows
425
+ attr_reader :puzzle, :grid, :box, :cursor, :clue, :dir
426
+ def initialize(date:Date.today)
427
+ @puzzle = Puzzle::Puzzle.new(date:date)
428
+ @grid = Grid.new(puzzle:puzzle)
429
+ @box = Cluebox.new(grid:grid)
430
+ @cursor = Cursor.new(grid:grid)
431
+ end
432
+ def setup(state:nil)
433
+ grid.draw
434
+ load_state(state:state)
435
+ set_clue(clue:puzzle.first_clue, mv:true)
436
+ update
437
+ self
438
+ end
439
+ def update
440
+ cursor.reset
441
+ grid.refresh
442
+ end
443
+ def redraw
444
+ grid.draw
445
+ grid.cells.flatten
446
+ .find_all{|c| c.buffer != " "}
447
+ .each{|c| c.unlock.write}
448
+ puzzle.check_all if $config[:auto_mark]
449
+ clue.activate
450
+ end
451
+ def move(y:0, x:0)
452
+ cursor.move(y:y, x:x)
453
+ if current_cell.blocked
454
+ move(y:y, x:x)
455
+ elsif outside_clue?
456
+ set_clue(clue:get_clue_at(**cursor.pos))
457
+ end
458
+ end
459
+ def insert_char(char:, advance:true)
460
+ addch(char:char)
461
+ move_after_insert(advance:advance)
462
+ check_current_clue
463
+ end
464
+ def delete_char(advance:true)
465
+ addch(char:" ").underline
466
+ advance_cursor(n:-1) if advance &&
467
+ !on_first_cell?
468
+ end
469
+ def next_clue(n:1)
470
+ n.times do
471
+ set_clue(clue:clue.next, mv:true)
472
+ end
473
+ next_clue if clue.done && !puzzle.complete?
474
+ end
475
+ def prev_clue(n:1)
476
+ n.times do
477
+ on_first_cell? ?
478
+ set_clue(clue:clue.prev, mv:true) :
479
+ to_start
480
+ end
481
+ prev_clue if clue.done && !puzzle.complete?
482
+ end
483
+ def to_start
484
+ cursor.set(**clue.coords.first)
485
+ end
486
+ def to_end
487
+ cursor.set(**clue.coords.last)
488
+ end
489
+ def swap_direction
490
+ set_clue(clue:get_clue_at(
491
+ **cursor.pos, dir:Pos.change_dir(dir)))
492
+ end
493
+ def save_state
494
+ [].tap do |state|
495
+ grid.cells.flatten.map do |cell|
496
+ state << { sq:cell.sq, char:cell.buffer } unless cell.blocked || cell.buffer == " "
497
+ end
498
+ end
499
+ end
500
+ def goto_clue(n:)
501
+ set_clue(clue:get_clue_by_index(i:n), mv:true)
502
+ end
503
+ def goto_cell(n:)
504
+ if n > 0 && n <= clue.length
505
+ cursor.set(**clue.cells[n-1].sq)
506
+ end
507
+ end
508
+ def clear_clue
509
+ clue.clear
510
+ clue.activate
511
+ end
512
+ def reveal_clue
513
+ clue.reveal
514
+ next_clue(n:1)
515
+ end
516
+ def clear_all_cells
517
+ grid.cells.flatten.each{|cell| cell.clear}
518
+ puzzle.clues.each{|clue| clue.done = false}
519
+ end
520
+ def advance_cursor(n:1)
521
+ case dir
522
+ when :a then move(x:n)
523
+ when :d then move(y:n)
524
+ end
525
+ end
526
+ private
527
+ def load_state(state:)
528
+ if state.exists?
529
+ state.chars.each do |s|
530
+ grid.cell(**s[:sq]).write(s[:char])
531
+ end
532
+ puzzle.check_all if $config[:auto_mark]
533
+ end
534
+ end
535
+ def set_clue(clue:, mv:false)
536
+ @clue.deactivate if @clue
537
+ @clue = clue
538
+ @dir = clue.dir
539
+ clue.activate
540
+ cursor.set(**clue.start.dup) if mv
541
+ box.show(clue:clue)
542
+ end
543
+ def current_cell
544
+ grid.cell(**cursor.pos)
545
+ end
546
+ def on_first_cell?
547
+ current_cell == clue.cells.first
548
+ end
549
+ def on_last_cell?
550
+ current_cell == clue.cells.last
551
+ end
552
+ def outside_clue?
553
+ !clue.has_cell?(**cursor.pos)
554
+ end
555
+ def get_clue_at(y:, x:, dir:@dir)
556
+ puzzle.get_clue(y:y, x:x, dir:dir)
557
+ end
558
+ def get_clue_by_index(i:)
559
+ puzzle.get_clue_by_index(i:i, dir:dir)
560
+ end
561
+ def addch(char:)
562
+ current_cell.write(char.upcase)
563
+ end
564
+ def move_after_insert(advance:)
565
+ if on_last_cell?
566
+ next_clue(n:1) if $config[:auto_advance]
567
+ elsif advance
568
+ advance_cursor(n:1)
569
+ end
570
+ end
571
+ def check_current_clue
572
+ clue.check if $config[:auto_mark]
573
+ end
574
+ end
575
+ class Cursor
576
+ attr_reader :grid, :pos
577
+ def initialize(grid:)
578
+ @grid = grid
579
+ end
580
+ def set(y:, x:)
581
+ @pos = Pos.mk(y,x)
582
+ end
583
+ def reset
584
+ grid.cell(**pos).focus
585
+ Curses.curs_set(1)
586
+ end
587
+ def move(y:, x:)
588
+ @pos[:y]+= y
589
+ @pos[:x]+= x
590
+ wrap
591
+ end
592
+ def wrap
593
+ pos[:x] += grid.sq[:x] while pos[:x] < 0
594
+ pos[:y] += grid.sq[:y] while pos[:y] < 0
595
+ pos[:x] -= grid.sq[:x] while pos[:x] >=
596
+ grid.sq[:x]
597
+ pos[:y] -= grid.sq[:y] while pos[:y] >=
598
+ grid.sq[:y]
599
+ end
600
+ end
601
+ class Game
602
+ include Database, Windows, Menus
603
+ attr_reader :state, :board, :top_b, :timer,
604
+ :bot_b, :ctrls , :date
605
+ attr_accessor :mode, :continue, :unsaved
606
+ def initialize(date:Date.today)
607
+ @date = date
608
+ @state = State.new(date:date)
609
+ @board = Board.new(date:date)
610
+ @top_b = Top_Bar.new(date:date)
611
+ @bot_b = Bottom_Bar.new
612
+ @timer = Timer.new(time:state.time, bar:top_b,
613
+ callback:->{board.update})
614
+ @ctrls = Controller.new(game:self)
615
+ @unsaved = false
616
+ @continue = true
617
+ setup
618
+ end
619
+ def play
620
+ if state.done
621
+ show_completed_menu
622
+ else
623
+ add_to_recents
624
+ game_and_timer_threads.map(&:join)
625
+ end
626
+ end
627
+ def setup
628
+ [top_b, bot_b].each(&:draw)
629
+ self.mode = :N
630
+ board.setup(state:state)
631
+ end
632
+ def unsaved=(bool)
633
+ @unsaved = bool
634
+ bot_b.unsaved(bool)
635
+ end
636
+ def mode=(mode)
637
+ @mode = mode
638
+ bot_b.mode(mode)
639
+ end
640
+ def user_input
641
+ board.grid.getch
642
+ end
643
+ def pause
644
+ timer.stop
645
+ Menus::Pause.new(game:self).choose_opt
646
+ end
647
+ def unpause
648
+ timer.start
649
+ board.redraw
650
+ end
651
+ def save
652
+ state.save(game:self)
653
+ bot_b.time_str(t:5, x:10, str:"Saved!")
654
+ self.unsaved = false
655
+ end
656
+ def reset_menu
657
+ timer.stop
658
+ Reset_Progress.new(game:self)
659
+ end
660
+ def reset
661
+ state.delete if state.exists?
662
+ board.clear_all_cells
663
+ timer.reset
664
+ end
665
+ def reveal
666
+ state.reveals+= 1
667
+ board.reveal_clue
668
+ end
669
+ def game_over
670
+ save if $config[:auto_save]
671
+ timer.stop
672
+ completed if board.puzzle.complete?
673
+ end
674
+ def exit
675
+ @continue = false
676
+ Screen.clear
677
+ end
678
+ private
679
+ def completed
680
+ save
681
+ log_score
682
+ show_completed_menu
683
+ end
684
+ def run
685
+ until game_finished?
686
+ ctrls.route(char:user_input)&.call
687
+ board.update
688
+ end
689
+ game_over
690
+ end
691
+ def game_finished?
692
+ board.puzzle.complete? || !continue
693
+ end
694
+ def game_and_timer_threads
695
+ [ Thread.new{run}, Thread.new{timer.start} ]
696
+ end
697
+ def show_completed_menu
698
+ Puzzle_Complete.new.choose_opt
699
+ end
700
+ def add_to_recents
701
+ Recents.new.add(date:date)
702
+ end
703
+ def log_score
704
+ Scores.new.add(game:self)
705
+ end
706
+ end
707
+ class Timer
708
+ attr_reader :time, :bar, :callback, :run
709
+ def initialize(time:0, bar:, callback:)
710
+ @time = Time.abs(time)
711
+ @bar, @callback = bar, callback
712
+ end
713
+ def start
714
+ Thread.new{tick}
715
+ @run = true
716
+ end
717
+ def stop
718
+ @run = false
719
+ end
720
+ def reset
721
+ @time = Time.abs(0)
722
+ @run = false
723
+ end
724
+ private
725
+ def tick
726
+ while @run
727
+ bar.add_str(x:-1, str:time_str)
728
+ callback.call
729
+ @time += 1
730
+ sleep(1)
731
+ end
732
+ end
733
+ def time_str
734
+ time.strftime("%T")
735
+ end
736
+ end
737
+ class Controller
738
+ attr_reader :game
739
+ def initialize(game:)
740
+ @game = game
741
+ end
742
+ def route(char:)
743
+ if is_ctrl_key?(char)
744
+ controls[:G][char.to_i]
745
+ elsif is_arrow_key?(char)
746
+ arrow(char)
747
+ else
748
+ case game.mode
749
+ when :N then normal(char:char)
750
+ when :I then insert(char:char)
751
+ end
752
+ end
753
+ end
754
+ def normal(char:, n:1)
755
+ if (?0..?9).cover?(char)
756
+ await_int(n:char.to_i)
757
+ else
758
+ controls(n)[:N][char]
759
+ end
760
+ end
761
+ def insert(char:)
762
+ case char
763
+ when 27 then ->{game.mode = :N}
764
+ when 127 then ->{game.board.delete_char}
765
+ when ?A..?z
766
+ ->{game.board.insert_char(char:char);
767
+ game.unsaved = true }
768
+ end
769
+ end
770
+ def is_ctrl_key?(char)
771
+ (1..26).cover?(char)
772
+ end
773
+ def is_arrow_key?(char)
774
+ (258..261).cover?(char)
775
+ end
776
+ def arrow(char)
777
+ mv = case char
778
+ when Curses::KEY_UP then {y:-1}
779
+ when Curses::KEY_DOWN then {y:1}
780
+ when Curses::KEY_LEFT then {x:-1}
781
+ when Curses::KEY_RIGHT then {x:1}
782
+ end
783
+ ->{game.board.move(**mv)}
784
+ end
785
+ def controls(n=1)
786
+ {
787
+ G:{
788
+ 3 => ->{game.exit},
789
+ 5 => ->{game.reset_menu.choose_opt},
790
+ 7 => ->{game.board.puzzle.check_all},
791
+ 9 => ->{game.board.swap_direction},
792
+ 16 => ->{game.pause},
793
+ 18 => ->{game.reveal},
794
+ 19 => ->{game.save}
795
+ },
796
+ N:{
797
+ ?j => ->{game.board.move(y:n)},
798
+ ?k => ->{game.board.move(y:n*-1)},
799
+ ?h => ->{game.board.move(x:n*-1)},
800
+ ?l => ->{game.board.move(x:n)},
801
+ ?i => ->{game.mode = :I},
802
+ ?I => ->{game.board.to_start; game.mode=:I},
803
+ ?w => ->{game.board.next_clue(n:n)},
804
+ ?a => ->{game.board.advance_cursor(n:1); game.mode=:I},
805
+ ?b => ->{game.board.prev_clue(n:n)},
806
+ ?e => ->{game.board.to_end},
807
+ ?r => ->{await_replace},
808
+ ?c => ->{await_delete; game.mode=:I},
809
+ ?d => ->{await_delete},
810
+ ?x => ->{game.board.delete_char(advance:false)}
811
+ }
812
+ }
813
+ end
814
+ def await_int(n:)
815
+ char = game.user_input
816
+ case char
817
+ when ?g then ->{game.board.goto_clue(n:n)}
818
+ when ?G then ->{game.board.goto_cell(n:n)}
819
+ when ?0..?9 then await_int(n:(10*n)+char.to_i)
820
+ else normal(char:char, n:n)
821
+ end
822
+ end
823
+ def await_replace
824
+ char = game.user_input
825
+ case char
826
+ when ?A..?z
827
+ game.board.insert_char(
828
+ char:char, advance:false)
829
+ end
830
+ end
831
+ def await_delete
832
+ case game.user_input
833
+ when ?w then game.board.clear_clue
834
+ end
835
+ end
836
+ end
837
+ end
838
+ end
839
+ end