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,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: []
|