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,135 @@
|
|
1
|
+
module Cliptic
|
2
|
+
module Menus
|
3
|
+
class Main < Interface::Menu
|
4
|
+
def opts
|
5
|
+
{
|
6
|
+
"Play Today" => ->{Cliptic::Main::Player::Game.new.play},
|
7
|
+
"Select Date"=> ->{Select_Date.new.choose_opt},
|
8
|
+
"This Week" => ->{This_Week.new.choose_opt},
|
9
|
+
"Recent Puzzles" => ->{Recent_Puzzles.new.choose_opt},
|
10
|
+
"High Scores"=> ->{High_Scores.new.choose_opt},
|
11
|
+
"Quit" => ->{exit}
|
12
|
+
}
|
13
|
+
end
|
14
|
+
def title
|
15
|
+
"Main Menu"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class Select_Date < Interface::Menu_With_Stats
|
19
|
+
attr_reader :opts
|
20
|
+
def initialize
|
21
|
+
set_date(date:Date.today)
|
22
|
+
super(height:7, sel:Interface::Date_Selector)
|
23
|
+
end
|
24
|
+
def title
|
25
|
+
"Select Date"
|
26
|
+
end
|
27
|
+
def ctrls
|
28
|
+
super.merge({
|
29
|
+
?h => ->{selector.cursor -= 1},
|
30
|
+
?l => ->{selector.cursor += 1},
|
31
|
+
?j => ->{inc_date(1)},
|
32
|
+
?k => ->{inc_date(-1)},
|
33
|
+
258 => ->{inc_date(1)},
|
34
|
+
259 => ->{inc_date(-1)},
|
35
|
+
260 => ->{selector.cursor -= 1},
|
36
|
+
261 => ->{selector.cursor += 1}
|
37
|
+
})
|
38
|
+
end
|
39
|
+
def stat_date
|
40
|
+
Date.new(*@opts.reverse)
|
41
|
+
end
|
42
|
+
private
|
43
|
+
def set_date(date:)
|
44
|
+
@opts = [] unless @opts
|
45
|
+
@opts[0] = date.day
|
46
|
+
@opts[1] = date.month
|
47
|
+
@opts[2] = date.year
|
48
|
+
end
|
49
|
+
def next_date(n)
|
50
|
+
@opts.dup.tap{|d| d[selector.cursor] += n }
|
51
|
+
end
|
52
|
+
def inc_date(n)
|
53
|
+
case selector.cursor
|
54
|
+
when 0 then inc_day(n)
|
55
|
+
when 1 then inc_month(n)
|
56
|
+
when 2 then @opts[2] += n
|
57
|
+
end
|
58
|
+
check_in_range
|
59
|
+
end
|
60
|
+
def inc_day(n)
|
61
|
+
next_date(n).tap do |date|
|
62
|
+
if valid?(date)
|
63
|
+
@opts[0]+= n
|
64
|
+
elsif date[0] == 0
|
65
|
+
set_date(date:stat_date-1)
|
66
|
+
elsif date[0] > 28
|
67
|
+
set_date(date:stat_date+1)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
def inc_month(n)
|
72
|
+
next_date(n).tap do |date|
|
73
|
+
if valid?(date)
|
74
|
+
@opts[1] += n
|
75
|
+
elsif date[1] == 0
|
76
|
+
set_date(date:stat_date << 1)
|
77
|
+
elsif date[1] == 13
|
78
|
+
set_date(date:stat_date >> 1)
|
79
|
+
elsif date[0] > 28
|
80
|
+
set_date(date:last_day_of_month(date:date))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
def valid?(date)
|
85
|
+
Date.valid_date?(*date.reverse)
|
86
|
+
end
|
87
|
+
def last_day_of_month(date:)
|
88
|
+
Date.new(date[2], date[1]+1, 1)-1
|
89
|
+
end
|
90
|
+
def check_in_range
|
91
|
+
set_date(date:Date.today) if date_late?
|
92
|
+
set_date(date:Date.today << 9) if date_early?
|
93
|
+
end
|
94
|
+
def date_late?
|
95
|
+
stat_date > Date.today
|
96
|
+
end
|
97
|
+
def date_early?
|
98
|
+
stat_date < Date.today<<9
|
99
|
+
end
|
100
|
+
end
|
101
|
+
class This_Week < Interface::Menu_With_Stats
|
102
|
+
def days
|
103
|
+
@days || 7.times.map{|i| Date.today - i}
|
104
|
+
end
|
105
|
+
def stat_date
|
106
|
+
days[selector.cursor]
|
107
|
+
end
|
108
|
+
def opts
|
109
|
+
@opts || days.map{|d| d.strftime("%A").ljust(9).center(12) + tickbox(d)}
|
110
|
+
end
|
111
|
+
def title
|
112
|
+
"This Week"
|
113
|
+
end
|
114
|
+
def tickbox(date)
|
115
|
+
"[#{Database::State.new(date:date).done ? Chars::Tick : " "}]"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
class Recent_Puzzles < Interface::SQL_Menu_With_Stats
|
119
|
+
def initialize
|
120
|
+
super(table:Database::Recents)
|
121
|
+
end
|
122
|
+
def title
|
123
|
+
"Recently Played"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
class High_Scores < Interface::SQL_Menu_With_Stats
|
127
|
+
def initialize
|
128
|
+
super(table:Database::Scores)
|
129
|
+
end
|
130
|
+
def title
|
131
|
+
"High Scores"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Cliptic
|
2
|
+
module Terminal
|
3
|
+
class Command
|
4
|
+
def self.run
|
5
|
+
ARGV.size > 0 ?
|
6
|
+
parse_args : main_menu
|
7
|
+
rescue StandardError => e
|
8
|
+
Curses.close_screen
|
9
|
+
abort(e.message)
|
10
|
+
end
|
11
|
+
private
|
12
|
+
def self.setup
|
13
|
+
Config::Setter.new.set
|
14
|
+
Screen.setup
|
15
|
+
at_exit{close}
|
16
|
+
end
|
17
|
+
def self.main_menu
|
18
|
+
setup
|
19
|
+
Cliptic::Menus::Main.new.choose_opt
|
20
|
+
end
|
21
|
+
def self.parse_args
|
22
|
+
case arg = ARGV.shift
|
23
|
+
when "reset", "-r" then Reset_Stats.route.call
|
24
|
+
when "today", "-t" then play(ARGV.shift.to_i)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def self.play(offset)
|
28
|
+
setup
|
29
|
+
Cliptic::Main::Player::
|
30
|
+
Game.new(date:Date.today+offset).play
|
31
|
+
end
|
32
|
+
def self.close
|
33
|
+
Curses.close_screen
|
34
|
+
puts "Thanks for playing!"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
class Reset_Stats
|
38
|
+
def self.route
|
39
|
+
if valid_options.include?(c = ARGV.shift)
|
40
|
+
->{confirm_reset(c)}
|
41
|
+
else
|
42
|
+
->{puts "Unknown option #{c}"}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
private
|
46
|
+
def self.valid_options
|
47
|
+
["scores", "all", "states", "recents"]
|
48
|
+
end
|
49
|
+
def self.confirm_reset(table)
|
50
|
+
puts prompt(table)
|
51
|
+
user_confirmed? ?
|
52
|
+
reset(table) :
|
53
|
+
puts("Wise choice")
|
54
|
+
end
|
55
|
+
def self.prompt(table)
|
56
|
+
<<~prompt
|
57
|
+
cliptic: Reset #{table}
|
58
|
+
Are you sure? This cannot be undone! [Y/n]
|
59
|
+
prompt
|
60
|
+
end
|
61
|
+
def self.user_confirmed?
|
62
|
+
gets.chomp === "Y"
|
63
|
+
end
|
64
|
+
def self.reset(table)
|
65
|
+
table == "all" ?
|
66
|
+
Database::Delete.all :
|
67
|
+
Database::Delete.table(table)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Cliptic
|
2
|
+
module Windows
|
3
|
+
class Window < Curses::Window
|
4
|
+
include Chars
|
5
|
+
attr_reader :y, :x, :line, :col
|
6
|
+
attr_reader :centered_y, :centered_x
|
7
|
+
def initialize(y:0, x:0, line:nil, col:nil)
|
8
|
+
@y, @x = wrap_dims(y:y, x:x)
|
9
|
+
@line,@col= center_pos(y:y,x:x,line:line,col:col)
|
10
|
+
@centered_y, @centered_x = [line, col]
|
11
|
+
.map{|pos| pos.nil?}
|
12
|
+
super(@y, @x, @line, @col)
|
13
|
+
keypad(true)
|
14
|
+
end
|
15
|
+
def draw(cp:$colors[:box]||0, clr:false)
|
16
|
+
erase if clr
|
17
|
+
setpos.color(cp)
|
18
|
+
1.upto(y) do |i|
|
19
|
+
line = case i
|
20
|
+
when 1 then top_border
|
21
|
+
when y then bottom_border
|
22
|
+
else side_border
|
23
|
+
end
|
24
|
+
self << line
|
25
|
+
end; setpos.color.noutrefresh
|
26
|
+
self
|
27
|
+
end
|
28
|
+
def color(cp=0)
|
29
|
+
color_set(cp); self
|
30
|
+
end
|
31
|
+
def bold(on=true)
|
32
|
+
on ?
|
33
|
+
attron(Curses::A_BOLD) :
|
34
|
+
attroff(Curses::A_BOLD)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
def wrap_str(str:, line:)
|
38
|
+
split_str(str).each_with_index do |l, i|
|
39
|
+
setpos(line+i, 2)
|
40
|
+
self << l
|
41
|
+
end; self
|
42
|
+
end
|
43
|
+
def setpos(y=0, x=0)
|
44
|
+
super(y,x); self
|
45
|
+
end
|
46
|
+
def add_str(str:,y:line,x:(@x-str.length)/2, cp:nil, bold:false)
|
47
|
+
color(cp) if cp
|
48
|
+
setpos(*wrap_str_dims(y:y, x:x, str:str))
|
49
|
+
bold(bold)
|
50
|
+
self << str
|
51
|
+
noutrefresh
|
52
|
+
reset_attrs
|
53
|
+
end
|
54
|
+
def clear
|
55
|
+
erase
|
56
|
+
noutrefresh
|
57
|
+
self
|
58
|
+
end
|
59
|
+
def time_str(str:, y:, x:(@x-str.length)/2, t:5, cp:nil, bold:false)
|
60
|
+
Thread.new{
|
61
|
+
add_str(str:str, y:y, x:x, cp:cp, bold:bold)
|
62
|
+
sleep(t)
|
63
|
+
add_str(str:" "*str.length, y:y, x:x, cp:cp, bold:bold)
|
64
|
+
}
|
65
|
+
self
|
66
|
+
end
|
67
|
+
def reset_attrs
|
68
|
+
color(0).bold(false)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
def refresh
|
72
|
+
super; self
|
73
|
+
end
|
74
|
+
def reset_pos
|
75
|
+
move(
|
76
|
+
*[centered_y, centered_x]
|
77
|
+
.zip(total_dims, [y, x], [line, col])
|
78
|
+
.map{|cent, tot, dim, pos| cent ? (tot-dim)/2 : pos}
|
79
|
+
)
|
80
|
+
refresh
|
81
|
+
end
|
82
|
+
def move(line:nil, col:nil)
|
83
|
+
super(*center_pos(y:y, x:x, line:line, col:col))
|
84
|
+
end
|
85
|
+
private
|
86
|
+
def wrap_dims(y:, x:)
|
87
|
+
[y, x].zip(total_dims)
|
88
|
+
.map{|dim, tot| dim <= 0 ? dim+tot : dim}
|
89
|
+
end
|
90
|
+
def wrap_str_dims(y:, x:, str:)
|
91
|
+
[y, x].zip(total_dims)
|
92
|
+
.map{|pos,tot|pos<0 ? tot-str.length+pos : pos}
|
93
|
+
end
|
94
|
+
def total_dims
|
95
|
+
[Curses.lines, Curses.cols]
|
96
|
+
end
|
97
|
+
def center_pos(y:, x:, line:, col:)
|
98
|
+
[line, col].zip(total_dims, [y, x])
|
99
|
+
.map{|pos, tot, dim| pos || ((tot-dim)/2)}
|
100
|
+
end
|
101
|
+
def split_str(str)
|
102
|
+
str.gsub(/(.{1,#{x-4}})(\s+|$\n?)|(.{1,#{x-4}})/, "\\1\\3\n").split("\n")
|
103
|
+
end
|
104
|
+
def top_border
|
105
|
+
LU+(HL*(x-2))+RU
|
106
|
+
end
|
107
|
+
def bottom_border
|
108
|
+
LL+(HL*(x-2))+RL
|
109
|
+
end
|
110
|
+
def side_border
|
111
|
+
VL+(' '*(x-2))+VL
|
112
|
+
end
|
113
|
+
end
|
114
|
+
class Grid < Window
|
115
|
+
attr_reader :sq, :cells
|
116
|
+
def initialize(y:, x:, line:nil, col:nil)
|
117
|
+
@sq = Pos.mk(y,x)
|
118
|
+
@y, @x = sq_to_dims(y:y, x:x)
|
119
|
+
super(y:@y, x:@x, line:line, col:col)
|
120
|
+
@cells = make_cells(**sq)
|
121
|
+
end
|
122
|
+
def draw(cp:$colors[:grid]||0)
|
123
|
+
setpos.color(cp)
|
124
|
+
1.upto(y) do |i|
|
125
|
+
line = case i
|
126
|
+
when 1 then top_border
|
127
|
+
when y then bottom_border
|
128
|
+
else i.even? ? side_border : inner_border
|
129
|
+
end
|
130
|
+
self << line
|
131
|
+
end; setpos.color
|
132
|
+
self
|
133
|
+
end
|
134
|
+
def add_str(str:, y:0, x:0)
|
135
|
+
str.chars.each_with_index do |char, i|
|
136
|
+
cell(y:y, x:x+i).write(char)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
def cell(y:, x:)
|
140
|
+
cells[y][x]
|
141
|
+
end
|
142
|
+
private
|
143
|
+
def sq_to_dims(y:, x:)
|
144
|
+
[ (2*y)+1, (4*x)+1 ]
|
145
|
+
end
|
146
|
+
def make_cells(y:, x:)
|
147
|
+
y.times.map{|iy| x.times.map{|ix|
|
148
|
+
Cell.new(sq:Pos.mk(iy,ix), grid:self)}}
|
149
|
+
end
|
150
|
+
def top_border
|
151
|
+
LU+(HL*3+TD)*(sq[:x]-1)+HL*3+RU
|
152
|
+
end
|
153
|
+
def bottom_border
|
154
|
+
LL+(HL*3+TU)*(sq[:x]-1)+HL*3+RL
|
155
|
+
end
|
156
|
+
def side_border
|
157
|
+
(VL+" "*3)*sq[:x]+VL
|
158
|
+
end
|
159
|
+
def inner_border
|
160
|
+
TR+(HL*3+XX)*(sq[:x]-1)+HL*3+TL
|
161
|
+
end
|
162
|
+
end
|
163
|
+
class Cell
|
164
|
+
attr_reader :sq, :grid, :pos
|
165
|
+
def initialize(sq:, grid:)
|
166
|
+
@sq, @grid, @pos = sq, grid, calc_abs_pos(**sq)
|
167
|
+
end
|
168
|
+
def focus(y:0, x:0)
|
169
|
+
grid.setpos(*[y,x].zip(pos.values).map(&:sum))
|
170
|
+
self
|
171
|
+
end
|
172
|
+
def write(char)
|
173
|
+
focus.grid << char; self
|
174
|
+
end
|
175
|
+
protected
|
176
|
+
def calc_abs_pos(y:, x:)
|
177
|
+
{ y:(2*y)+1, x:(4*x)+2 }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
class Bar < Window
|
181
|
+
attr_reader :bg_col
|
182
|
+
def initialize(line:, bg_col:$colors[:bar])
|
183
|
+
super(y:1, x:0, line:line, col:0)
|
184
|
+
@bg_col = bg_col
|
185
|
+
end
|
186
|
+
def add_str(y:0, x:, str:, bold:false, cp:bg_col)
|
187
|
+
super(y:0, x:x, str:str, bold:bold, cp:cp)
|
188
|
+
end
|
189
|
+
def time_str(x:, str:, t:5, cp:bg_col, bold:false)
|
190
|
+
super(y:0, x:x, str:str, t:5, cp:cp, bold:bold)
|
191
|
+
end
|
192
|
+
def draw
|
193
|
+
bkgd(Curses.color_pair(bg_col)); self
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cliptic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christian Welham
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-07-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.4.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.4.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: curb
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.11
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.9.11
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.4.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.4.2
|
55
|
+
description: A terminal user interface to fetch and play cryptic crosswords
|
56
|
+
email:
|
57
|
+
- welhamm@gmail.com
|
58
|
+
executables:
|
59
|
+
- cliptic
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- CHANGELOG.md
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- bin/cliptic
|
71
|
+
- bin/console
|
72
|
+
- bin/setup
|
73
|
+
- cliptic.gemspec
|
74
|
+
- lib/cliptic.rb
|
75
|
+
- lib/cliptic/config.rb
|
76
|
+
- lib/cliptic/database.rb
|
77
|
+
- lib/cliptic/interface.rb
|
78
|
+
- lib/cliptic/lib.rb
|
79
|
+
- lib/cliptic/main.rb
|
80
|
+
- lib/cliptic/menus.rb
|
81
|
+
- lib/cliptic/terminal.rb
|
82
|
+
- lib/cliptic/version.rb
|
83
|
+
- lib/cliptic/windows.rb
|
84
|
+
homepage: https://github.com/apexatoll/cliptic
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata:
|
88
|
+
homepage_uri: https://github.com/apexatoll/cliptic
|
89
|
+
source_code_uri: https://github.com/apexatoll/cliptic
|
90
|
+
changelog_uri: https://github.com/apexatoll/cliptic/CHANGELOG.md
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 2.4.0
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.2.15
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Terminal-based cryptic crossword player
|
110
|
+
test_files: []
|