cliptic 0.1.0

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