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