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.
- 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
|