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
@@ -0,0 +1,247 @@
|
|
1
|
+
module Cliptic
|
2
|
+
module Database
|
3
|
+
Dir_Path = "#{Dir.home}/.config/cliptic/db"
|
4
|
+
File_Path = "#{Dir_Path}/cliptic.db"
|
5
|
+
class SQL
|
6
|
+
attr_reader :db, :table
|
7
|
+
def initialize(table:)
|
8
|
+
make_db_dir
|
9
|
+
@table = table
|
10
|
+
@db = SQLite3::Database.open(File_Path)
|
11
|
+
db.results_as_hash = true
|
12
|
+
self
|
13
|
+
end
|
14
|
+
def make_table
|
15
|
+
db.execute(sql_make)
|
16
|
+
end
|
17
|
+
def select(cols:"*", where:nil, order:false, limit:false)
|
18
|
+
db.execute(
|
19
|
+
sql_select(cols:cols, where:where,
|
20
|
+
order:order, limit:limit),
|
21
|
+
where&.values&.map(&:to_s)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
def insert(values:)
|
25
|
+
db.execute(sql_insert(values:values),
|
26
|
+
values.values)
|
27
|
+
end
|
28
|
+
def update(values:, where:)
|
29
|
+
db.execute(sql_update(values:values, where:where), [values.values], [where.values])
|
30
|
+
end
|
31
|
+
def delete(where:nil)
|
32
|
+
db.execute(sql_delete(where:where), where&.values)
|
33
|
+
end
|
34
|
+
def drop
|
35
|
+
db.execute("DROP TABLE #{table}")
|
36
|
+
end
|
37
|
+
private
|
38
|
+
def make_db_dir
|
39
|
+
FileUtils.mkdir_p(Dir_Path) unless Dir.exist?(Dir_Path)
|
40
|
+
end
|
41
|
+
def sql_make
|
42
|
+
"CREATE TABLE IF NOT EXISTS #{table}(#{
|
43
|
+
cols.map{|col,type|"#{col} #{type}"}.join(", ")
|
44
|
+
})"
|
45
|
+
end
|
46
|
+
def sql_select(cols:, where:, order:, limit:)
|
47
|
+
"SELECT #{cols} FROM #{table}" +
|
48
|
+
(where ? where_str(where) : "") +
|
49
|
+
(order ? order_str(order) : "") +
|
50
|
+
(limit ? " LIMIT #{limit}" : "")
|
51
|
+
end
|
52
|
+
def sql_insert(values:)
|
53
|
+
<<~sql
|
54
|
+
INSERT INTO #{table}(#{values.keys.join(", ")})
|
55
|
+
VALUES (#{Array.new(values.length, "?").join(", ")})
|
56
|
+
sql
|
57
|
+
end
|
58
|
+
def sql_update(values:, where:)
|
59
|
+
<<~sql
|
60
|
+
UPDATE #{table}
|
61
|
+
SET #{placeholder(values.keys, ", ")}
|
62
|
+
WHERE #{placeholder(where.keys)}
|
63
|
+
sql
|
64
|
+
end
|
65
|
+
def sql_delete(where:)
|
66
|
+
<<~sql
|
67
|
+
DELETE FROM #{table} #{where ? where_str(where) : ""}
|
68
|
+
sql
|
69
|
+
end
|
70
|
+
def where_str(where)
|
71
|
+
" WHERE #{placeholder(where.keys)}"
|
72
|
+
end
|
73
|
+
def order_str(order)
|
74
|
+
" ORDER BY #{order.keys
|
75
|
+
.map{|k| "#{k} #{order[k]}"}
|
76
|
+
.join(", ")}"
|
77
|
+
end
|
78
|
+
def placeholder(keys, glue=" AND ")
|
79
|
+
keys.map{|k| "#{k} = ?"}.join(glue)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
class Delete
|
83
|
+
def self.table(table)
|
84
|
+
SQL.new(table:table).drop
|
85
|
+
end
|
86
|
+
def self.all
|
87
|
+
File.delete(File_Path)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
class State < SQL
|
91
|
+
include Chars
|
92
|
+
attr_reader :date, :time, :chars, :n_done, :n_tot,
|
93
|
+
:reveals, :done
|
94
|
+
attr_accessor :reveals
|
95
|
+
def initialize(date:Date.today)
|
96
|
+
super(table:"states").make_table
|
97
|
+
@date = date
|
98
|
+
set
|
99
|
+
end
|
100
|
+
def cols
|
101
|
+
{
|
102
|
+
date: :DATE, time: :INT, chars: :TEXT,
|
103
|
+
n_done: :INT, n_tot: :INT, reveals: :INT,
|
104
|
+
done: :INT
|
105
|
+
}
|
106
|
+
end
|
107
|
+
def exists?
|
108
|
+
@exists || query.count > 0
|
109
|
+
end
|
110
|
+
def save(game:)
|
111
|
+
exists? ? save_existing(game) : save_new(game)
|
112
|
+
end
|
113
|
+
def delete
|
114
|
+
super(where:{date:date.to_s})
|
115
|
+
@exists = false
|
116
|
+
set
|
117
|
+
end
|
118
|
+
private
|
119
|
+
def set
|
120
|
+
@time,@chars,@n_done,@n_tot,@reveals,@done =
|
121
|
+
exists? ? instantiate : blank
|
122
|
+
end
|
123
|
+
def instantiate
|
124
|
+
[
|
125
|
+
query[0]["time"].to_i,
|
126
|
+
parse_chars(query[0]["chars"]),
|
127
|
+
query[0]["n_done"].to_i,
|
128
|
+
query[0]["n_tot"].to_i,
|
129
|
+
query[0]["reveals"].to_i,
|
130
|
+
query[0]["done"].to_i == 1
|
131
|
+
]
|
132
|
+
end
|
133
|
+
def blank
|
134
|
+
[ 0, false, nil, nil, 0, false ]
|
135
|
+
end
|
136
|
+
def query
|
137
|
+
@query || select(where:{date:date})
|
138
|
+
end
|
139
|
+
def parse_chars(str)
|
140
|
+
JSON.parse(str, symbolize_names:true)
|
141
|
+
end
|
142
|
+
def build(game:)
|
143
|
+
{
|
144
|
+
date: date.to_s,
|
145
|
+
time: game.timer.time.to_i,
|
146
|
+
chars: gen_chars(game),
|
147
|
+
n_done: game.board.puzzle.n_clues_done,
|
148
|
+
n_tot: game.board.puzzle.n_clues,
|
149
|
+
reveals: reveals,
|
150
|
+
done: game.board.puzzle.complete? ? 1 : 0
|
151
|
+
}
|
152
|
+
end
|
153
|
+
def gen_chars(game)
|
154
|
+
JSON.generate(game.board.save_state)
|
155
|
+
end
|
156
|
+
def save_existing(game)
|
157
|
+
update(where:{date:date.to_s}, values:build(game:game))
|
158
|
+
end
|
159
|
+
def save_new(game)
|
160
|
+
insert(values:build(game:game))
|
161
|
+
@exists = true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
class Stats < State
|
165
|
+
def initialize(date:Date.today)
|
166
|
+
super(date:date)
|
167
|
+
end
|
168
|
+
def stats_str
|
169
|
+
(exists? ? exist_str : new_str).split("\n")
|
170
|
+
end
|
171
|
+
def exist_str
|
172
|
+
<<~stats
|
173
|
+
Time #{VL} #{Time.abs(time).to_s}
|
174
|
+
Clues #{VL} #{n_done}/#{n_tot}
|
175
|
+
Done #{VL} [#{done ? Tick : " "}]
|
176
|
+
stats
|
177
|
+
end
|
178
|
+
def new_str
|
179
|
+
"\n Not attempted\n"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
class Recents < SQL
|
183
|
+
attr_reader :date
|
184
|
+
def initialize
|
185
|
+
super(table:"recents").make_table
|
186
|
+
end
|
187
|
+
def cols
|
188
|
+
{
|
189
|
+
date: :DATE,
|
190
|
+
play_date: :DATE,
|
191
|
+
play_time: :TIME
|
192
|
+
}
|
193
|
+
end
|
194
|
+
def select_list
|
195
|
+
select(cols:"*", order:{play_date:"DESC", play_time:"DESC"}, limit:10)
|
196
|
+
end
|
197
|
+
def add(date:)
|
198
|
+
@date = date
|
199
|
+
exists? ? add_existing : add_new
|
200
|
+
end
|
201
|
+
def exists?
|
202
|
+
select(where:{date:date.to_s}).count > 0
|
203
|
+
end
|
204
|
+
def add_new
|
205
|
+
insert(values:build)
|
206
|
+
end
|
207
|
+
def add_existing
|
208
|
+
update(values:build, where:{date:date.to_s})
|
209
|
+
end
|
210
|
+
def build
|
211
|
+
{
|
212
|
+
date: date.to_s,
|
213
|
+
play_date: Date.today.to_s,
|
214
|
+
play_time: Time.now.strftime("%T")
|
215
|
+
}
|
216
|
+
end
|
217
|
+
end
|
218
|
+
class Scores < SQL
|
219
|
+
def initialize
|
220
|
+
super(table:"scores").make_table
|
221
|
+
end
|
222
|
+
def cols
|
223
|
+
{
|
224
|
+
date: :DATE,
|
225
|
+
date_done: :DATE,
|
226
|
+
time: :TEXT,
|
227
|
+
reveals: :INT
|
228
|
+
}
|
229
|
+
end
|
230
|
+
def add(game:)
|
231
|
+
insert(values:build(game:game))
|
232
|
+
end
|
233
|
+
def select_list
|
234
|
+
select(cols:"*", where:{reveals:0},
|
235
|
+
order:{time:"ASC"}, limit:10)
|
236
|
+
end
|
237
|
+
def build(game:)
|
238
|
+
{
|
239
|
+
date:game.date.to_s,
|
240
|
+
date_done:Date.today.to_s,
|
241
|
+
time:game.timer.time.strftime("%T"),
|
242
|
+
reveals:game.state.reveals
|
243
|
+
}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
module Cliptic
|
2
|
+
module Interface
|
3
|
+
class Top_Bar < Windows::Bar
|
4
|
+
attr_reader :date
|
5
|
+
def initialize(date:Date.today)
|
6
|
+
super(line:0)
|
7
|
+
@date = date
|
8
|
+
end
|
9
|
+
def draw
|
10
|
+
super
|
11
|
+
add_str(x:1, str:title, bold:true)
|
12
|
+
add_str(x:title.length+2, str:date.to_long)
|
13
|
+
end
|
14
|
+
def title
|
15
|
+
"cliptic:"
|
16
|
+
end
|
17
|
+
def reset_pos
|
18
|
+
move(line:0, col:0)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
class Bottom_Bar < Windows::Bar
|
22
|
+
def initialize
|
23
|
+
super(line:Curses.lines-1)
|
24
|
+
end
|
25
|
+
def draw
|
26
|
+
super
|
27
|
+
noutrefresh
|
28
|
+
end
|
29
|
+
def reset_pos
|
30
|
+
move(line:Curses.lines-1, col:0)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class Logo < Windows::Grid
|
34
|
+
attr_reader :text
|
35
|
+
def initialize(line:, text:"CLIptic")
|
36
|
+
super(y:1, x:text.length, line:line)
|
37
|
+
@text = text
|
38
|
+
end
|
39
|
+
def draw(cp_grid:$colors[:logo_grid],
|
40
|
+
cp_text:$colors[:logo_text], bold:true)
|
41
|
+
super(cp:cp_grid)
|
42
|
+
bold(bold).color(cp_text)
|
43
|
+
add_str(str:text)
|
44
|
+
reset_attrs
|
45
|
+
refresh
|
46
|
+
end
|
47
|
+
end
|
48
|
+
class Menu_Box < Windows::Window
|
49
|
+
include Interface
|
50
|
+
attr_reader :logo, :title, :top_b, :bot_b, :draw_bars
|
51
|
+
def initialize(y:, title:false)
|
52
|
+
@logo = Logo.new(line:line+1)
|
53
|
+
@title = title
|
54
|
+
super(y:y, x:logo.x+4, line:line, col:nil)
|
55
|
+
@top_b = Top_Bar.new
|
56
|
+
@bot_b = Bottom_Bar.new
|
57
|
+
@draw_bars = true
|
58
|
+
end
|
59
|
+
def draw
|
60
|
+
super
|
61
|
+
[top_b, bot_b].each(&:draw) if draw_bars
|
62
|
+
logo.draw
|
63
|
+
add_title if title
|
64
|
+
self
|
65
|
+
end
|
66
|
+
def line
|
67
|
+
(Curses.lines-15)/2
|
68
|
+
end
|
69
|
+
def add_title(y:4, str:title, cp:$colors[:title], bold:true)
|
70
|
+
add_str(y:y, str:str, cp:cp, bold:bold)
|
71
|
+
end
|
72
|
+
def reset_pos
|
73
|
+
move(line:line)
|
74
|
+
logo.move(line:line+1)
|
75
|
+
[top_b, bot_b].each(&:reset_pos)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
class Resizer < Menu_Box
|
79
|
+
def initialize
|
80
|
+
super(y:8, title:title)
|
81
|
+
end
|
82
|
+
def title
|
83
|
+
"Screen too small"
|
84
|
+
end
|
85
|
+
def draw
|
86
|
+
Screen.clear
|
87
|
+
reset_pos
|
88
|
+
super
|
89
|
+
wrap_str(str:prompt, line:5)
|
90
|
+
refresh
|
91
|
+
end
|
92
|
+
def show
|
93
|
+
while Screen.too_small?
|
94
|
+
draw; getch
|
95
|
+
end
|
96
|
+
end
|
97
|
+
def prompt
|
98
|
+
"Screen too small. Increase screen size to run cliptic."
|
99
|
+
end
|
100
|
+
end
|
101
|
+
class Selector < Windows::Window
|
102
|
+
attr_reader :opts, :ctrls, :run, :tick
|
103
|
+
attr_accessor :cursor
|
104
|
+
def initialize(opts:, ctrls:, x:, line:,
|
105
|
+
tick:nil, y:opts.length, col:nil)
|
106
|
+
super(y:y, x:x, line:line, col:col)
|
107
|
+
@opts, @ctrls, @tick = opts, ctrls, tick
|
108
|
+
@cursor, @run = 0, true
|
109
|
+
end
|
110
|
+
def select
|
111
|
+
while @run
|
112
|
+
draw
|
113
|
+
ctrls[getch]&.call
|
114
|
+
end
|
115
|
+
end
|
116
|
+
def stop
|
117
|
+
@run = false
|
118
|
+
end
|
119
|
+
def cursor=(n)
|
120
|
+
@cursor = Pos.wrap(val:n,min:0,max:opts.length-1)
|
121
|
+
end
|
122
|
+
private
|
123
|
+
def draw
|
124
|
+
Curses.curs_set(0)
|
125
|
+
setpos
|
126
|
+
opts.each_with_index do |opt, i|
|
127
|
+
standout if cursor == i
|
128
|
+
self << format_opt(opt)
|
129
|
+
standend
|
130
|
+
end
|
131
|
+
tick.call if tick
|
132
|
+
refresh
|
133
|
+
end
|
134
|
+
def format_opt(opt)
|
135
|
+
opt.to_s.center(x)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
class Date_Selector < Selector
|
139
|
+
def initialize(opts:, ctrls:, line:, x:18, tick:)
|
140
|
+
super(y:1, x:18, opts:opts,
|
141
|
+
ctrls:ctrls, line:line, tick:tick)
|
142
|
+
end
|
143
|
+
def format_opt(opt)
|
144
|
+
opt.to_s.rjust(2, "0").center(6)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
class Stat_Window < Windows::Window
|
148
|
+
def initialize(y:5, x:33, line:)
|
149
|
+
super(y:y, x:x, line:line)
|
150
|
+
end
|
151
|
+
def show(date:, cp:$colors[:stats])
|
152
|
+
draw(clr:true).color(cp)
|
153
|
+
get_stats(date:date).each_with_index do |line, i|
|
154
|
+
setpos(i+1, 8)
|
155
|
+
self << line
|
156
|
+
end
|
157
|
+
color.noutrefresh
|
158
|
+
end
|
159
|
+
def get_stats(date:)
|
160
|
+
Database::Stats.new(date:date).stats_str
|
161
|
+
end
|
162
|
+
end
|
163
|
+
class Menu < Menu_Box
|
164
|
+
attr_reader :selector, :height
|
165
|
+
def initialize(height:opts.length+6,
|
166
|
+
sel:Selector, sel_opts:opts.keys,
|
167
|
+
tick:nil, **)
|
168
|
+
super(y:height, title:title)
|
169
|
+
@height = height
|
170
|
+
@selector = sel.new(opts:sel_opts, ctrls:ctrls, line:line+5, x:logo.x, tick:tick)
|
171
|
+
end
|
172
|
+
def choose_opt
|
173
|
+
show
|
174
|
+
selector.select
|
175
|
+
end
|
176
|
+
def enter(pre_proc:->{hide}, post_proc:->{show})
|
177
|
+
pre_proc.call if pre_proc
|
178
|
+
opts.values[selector.cursor]&.call
|
179
|
+
post_proc.call if post_proc
|
180
|
+
end
|
181
|
+
def back(post_proc:->{hide})
|
182
|
+
selector.stop
|
183
|
+
post_proc.call if post_proc
|
184
|
+
end
|
185
|
+
def show
|
186
|
+
draw
|
187
|
+
end
|
188
|
+
def hide
|
189
|
+
clear
|
190
|
+
end
|
191
|
+
def ctrls
|
192
|
+
{
|
193
|
+
?j => ->{selector.cursor += 1},
|
194
|
+
?k => ->{selector.cursor -= 1},
|
195
|
+
258 => ->{selector.cursor += 1},
|
196
|
+
259 => ->{selector.cursor -= 1},
|
197
|
+
10 => ->{enter},
|
198
|
+
?q => ->{back},
|
199
|
+
3 => ->{back},
|
200
|
+
Curses::KEY_RESIZE =>
|
201
|
+
->{Screen.redraw(cb:->{redraw})}
|
202
|
+
}
|
203
|
+
end
|
204
|
+
def reset_pos
|
205
|
+
super
|
206
|
+
selector.move(line:line+5)
|
207
|
+
end
|
208
|
+
def redraw
|
209
|
+
reset_pos
|
210
|
+
draw
|
211
|
+
end
|
212
|
+
end
|
213
|
+
class Menu_With_Stats < Menu
|
214
|
+
attr_reader :stat_win
|
215
|
+
def initialize(height:opts.length+6, sel:Selector, **)
|
216
|
+
super(height:height, sel:sel, sel_opts:opts,
|
217
|
+
tick:->{update_stats})
|
218
|
+
@stat_win = Stat_Window.new(line:line+height)
|
219
|
+
end
|
220
|
+
def update_stats
|
221
|
+
stat_win.show(date:stat_date)
|
222
|
+
end
|
223
|
+
def hide
|
224
|
+
super
|
225
|
+
stat_win.clear
|
226
|
+
end
|
227
|
+
def enter
|
228
|
+
hide
|
229
|
+
Main::Player::Game.new(date:stat_date).play
|
230
|
+
show
|
231
|
+
end
|
232
|
+
def reset_pos
|
233
|
+
super
|
234
|
+
stat_win.move(line:line+height)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
class SQL_Menu_With_Stats < Menu_With_Stats
|
238
|
+
include Database
|
239
|
+
attr_reader :dates
|
240
|
+
def initialize(table:)
|
241
|
+
@dates = table.new
|
242
|
+
.select_list.map{|d| Date.parse(d[0])}
|
243
|
+
super
|
244
|
+
end
|
245
|
+
def opts
|
246
|
+
dates.map{|d| d.to_long} || [nil]
|
247
|
+
end
|
248
|
+
def stat_date
|
249
|
+
dates[selector.cursor]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
class Yes_No_Menu < Menu
|
253
|
+
attr_reader :yes, :no, :post_proc
|
254
|
+
def initialize(yes:, no:->{back}, post_proc:nil, title:nil)
|
255
|
+
super
|
256
|
+
@title = title
|
257
|
+
@yes, @no, @post_proc = yes, no, post_proc
|
258
|
+
end
|
259
|
+
def opts
|
260
|
+
{
|
261
|
+
"Yes" => ->{yes.call; back; post},
|
262
|
+
"No" => ->{no.call; post}
|
263
|
+
}
|
264
|
+
end
|
265
|
+
def post
|
266
|
+
post_proc.call if post_proc
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|