cliptic 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +4 -0
- data/bin/cliptic +5 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cliptic.gemspec +25 -0
- data/lib/cliptic.rb +53 -0
- data/lib/cliptic/config.rb +99 -0
- data/lib/cliptic/database.rb +247 -0
- data/lib/cliptic/interface.rb +270 -0
- data/lib/cliptic/lib.rb +64 -0
- data/lib/cliptic/main.rb +839 -0
- data/lib/cliptic/menus.rb +135 -0
- data/lib/cliptic/terminal.rb +72 -0
- data/lib/cliptic/version.rb +5 -0
- data/lib/cliptic/windows.rb +197 -0
- metadata +110 -0
data/lib/cliptic/lib.rb
ADDED
@@ -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
|
data/lib/cliptic/main.rb
ADDED
@@ -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
|