cliptic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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