basher-basher 0.1.1
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 +12 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +37 -0
- data/Rakefile +10 -0
- data/basher.gemspec +29 -0
- data/bin/basher +31 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/lib/basher.rb +21 -0
- data/lib/basher/cursor.rb +59 -0
- data/lib/basher/dictionary.rb +50 -0
- data/lib/basher/game.rb +234 -0
- data/lib/basher/handler.rb +34 -0
- data/lib/basher/level.rb +131 -0
- data/lib/basher/refinements/string.rb +15 -0
- data/lib/basher/state.rb +46 -0
- data/lib/basher/timer.rb +69 -0
- data/lib/basher/ui.rb +125 -0
- data/lib/basher/ui/base_view.rb +28 -0
- data/lib/basher/ui/current_word_view.rb +38 -0
- data/lib/basher/ui/debug_view.rb +53 -0
- data/lib/basher/ui/info_view.rb +35 -0
- data/lib/basher/ui/loading_view.rb +11 -0
- data/lib/basher/ui/menu_view.rb +26 -0
- data/lib/basher/ui/progress_view.rb +29 -0
- data/lib/basher/ui/remaining_words_view.rb +34 -0
- data/lib/basher/ui/score_view.rb +41 -0
- data/lib/basher/ui/title_view.rb +37 -0
- data/lib/basher/version.rb +3 -0
- data/lib/basher/word.rb +40 -0
- metadata +189 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module Basher
|
2
|
+
class Handler
|
3
|
+
ALPHABET = ('a'..'z').to_a.freeze
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_reader :bindings
|
7
|
+
|
8
|
+
def bind(*keys, &action)
|
9
|
+
@bindings ||= {}
|
10
|
+
keys.map(&:to_sym).each do |input|
|
11
|
+
@bindings[input] ||= []
|
12
|
+
@bindings[input] << action
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :bindings
|
18
|
+
|
19
|
+
def initialize(custom_bindings = {})
|
20
|
+
@bindings = self.class.bindings.merge(custom_bindings)
|
21
|
+
end
|
22
|
+
|
23
|
+
def invoke(input)
|
24
|
+
bindings.fetch(input.to_sym, []).map do |b|
|
25
|
+
b.call(input)
|
26
|
+
end.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def letter?(input)
|
30
|
+
return false if input.size != 1
|
31
|
+
input =~ /[[:alpha:]]/
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/basher/level.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Basher
|
4
|
+
# Handles the initialization of a single level in the game.
|
5
|
+
# The defining characteristic of a level is it's #difficulty, which
|
6
|
+
# controls the number of the words in the level, and their size.
|
7
|
+
#
|
8
|
+
# TODO: Extract constants to global configuration values.
|
9
|
+
class Level
|
10
|
+
# Delegate cursor methods to the word itself.
|
11
|
+
extend Forwardable
|
12
|
+
def_delegators :cursor, :position, :previous,
|
13
|
+
:remaining, :advance!, :rewind!, :finished?
|
14
|
+
|
15
|
+
def_delegator :cursor, :item, :word
|
16
|
+
|
17
|
+
# Biggest word size
|
18
|
+
MAX_WORD_SIZE = 15.freeze
|
19
|
+
|
20
|
+
# Word sizes
|
21
|
+
WORD_SIZES = (3..MAX_WORD_SIZE).freeze
|
22
|
+
|
23
|
+
# How many words per level
|
24
|
+
WORDS_PER_LEVEL = 8.freeze
|
25
|
+
|
26
|
+
# Use this attribute to determine the number of the words,
|
27
|
+
# and the length of the words.
|
28
|
+
attr_reader :difficulty
|
29
|
+
attr_reader :words
|
30
|
+
attr_reader :cursor
|
31
|
+
attr_reader :timer
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def start(difficulty, &on_end)
|
35
|
+
level = self.new(difficulty)
|
36
|
+
|
37
|
+
level.start do
|
38
|
+
on_end.call
|
39
|
+
end
|
40
|
+
|
41
|
+
level
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a Level instance with the default difficulty of 1.
|
46
|
+
def initialize(difficulty = 1)
|
47
|
+
@difficulty = difficulty || 1
|
48
|
+
pick_words!
|
49
|
+
@cursor = Cursor.new(words)
|
50
|
+
@timer = Timer.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def sizes
|
54
|
+
WORD_SIZES
|
55
|
+
end
|
56
|
+
|
57
|
+
def weights
|
58
|
+
sizes.map { |size| calculate(size) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def chances
|
62
|
+
weights.map { |weight| (weight / total_weight * 100.0).round(2) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get an array of words that are calculated based on the difficulty.
|
66
|
+
# The bigger the difficulty, the bigger the words.
|
67
|
+
def pick_words!(words_per_level = WORDS_PER_LEVEL)
|
68
|
+
@words = pick(words_per_level).map do |size|
|
69
|
+
Basher::Word.new(Basher::Dictionary.random_word(size))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def pick(words = 15)
|
74
|
+
1.upto(words).collect { roll }
|
75
|
+
end
|
76
|
+
|
77
|
+
def time_limit
|
78
|
+
[((difficulty + 2) * 100.to_f / (average_word_size.to_f ** 2)).ceil, 20].min * 1000
|
79
|
+
end
|
80
|
+
|
81
|
+
def start
|
82
|
+
timer.start
|
83
|
+
|
84
|
+
@thread = Thread.new do
|
85
|
+
begin
|
86
|
+
sleep 0.005 while timer.total_elapsed <= time_limit
|
87
|
+
timer.stop
|
88
|
+
yield
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def pause
|
94
|
+
timer.stop
|
95
|
+
end
|
96
|
+
|
97
|
+
def finish
|
98
|
+
timer.stop
|
99
|
+
@thread.terminate if !@thread.nil? && @thread.alive?
|
100
|
+
end
|
101
|
+
|
102
|
+
def average_word_size
|
103
|
+
words.reduce(0) { |sum, w| sum += w.string.size } / words.size
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def total_weight
|
109
|
+
weights.reduce(:+)
|
110
|
+
end
|
111
|
+
|
112
|
+
def roll
|
113
|
+
sizes_and_weights = sizes.zip(weights)
|
114
|
+
|
115
|
+
loop do
|
116
|
+
sizes_and_weights.shuffle.each do |tuple|
|
117
|
+
size, weight = *tuple
|
118
|
+
|
119
|
+
chance = (weight / weights.reduce(:+) * 100.0).round(2)
|
120
|
+
rolled = (rand * 100).round(2)
|
121
|
+
return size if rolled <= chance
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def calculate(size)
|
127
|
+
weight = (difficulty / (size / 2) ) ** size
|
128
|
+
weight.round(2)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'artii'
|
2
|
+
|
3
|
+
module Basher
|
4
|
+
module StringRefinements
|
5
|
+
refine String do
|
6
|
+
def ascii(font: 'broadway')
|
7
|
+
Artii::Base.new(font: font).asciify(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def ascii_size(font: 'broadway')
|
11
|
+
ascii(font: font).lines.map(&:size).max
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/basher/state.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Basher
|
2
|
+
class State
|
3
|
+
TRANSITIONS = {
|
4
|
+
loading: %i(menu in_game),
|
5
|
+
menu: %i(loading),
|
6
|
+
in_game: %i(paused score),
|
7
|
+
paused: %i(in_game menu),
|
8
|
+
score: %i(menu)
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def all
|
13
|
+
TRANSITIONS.keys
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :current
|
18
|
+
attr_accessor :difficulty
|
19
|
+
|
20
|
+
def initialize(initial_state = :loading)
|
21
|
+
@current = initial_state
|
22
|
+
end
|
23
|
+
|
24
|
+
TRANSITIONS.keys.each do |state|
|
25
|
+
define_method "#{state}?" do
|
26
|
+
current == state
|
27
|
+
end
|
28
|
+
|
29
|
+
define_method "#{state}!" do
|
30
|
+
@current = state
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def transitions
|
35
|
+
TRANSITIONS.fetch(current, [])
|
36
|
+
end
|
37
|
+
|
38
|
+
def transition_to(state)
|
39
|
+
@current = state if transitions.include?(state)
|
40
|
+
end
|
41
|
+
|
42
|
+
def current_difficulty
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/basher/timer.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Basher
|
2
|
+
class Timer
|
3
|
+
attr_reader :started_at, :stopped_at
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset
|
7
|
+
end
|
8
|
+
|
9
|
+
# Milliseconds
|
10
|
+
def elapsed
|
11
|
+
return @total_elapsed unless running?
|
12
|
+
((looking_at - started_at) * 1000).ceil
|
13
|
+
end
|
14
|
+
|
15
|
+
def total_elapsed
|
16
|
+
return @total_elapsed unless running?
|
17
|
+
@total_elapsed + elapsed
|
18
|
+
end
|
19
|
+
|
20
|
+
def total_elapsed_in_seconds
|
21
|
+
(total_elapsed.to_f / 1000).round
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_elapsed_humanized
|
25
|
+
seconds = total_elapsed_in_seconds
|
26
|
+
[[60, :seconds], [60, :minutes], [24, :hours]].map do |count, name|
|
27
|
+
if seconds > 0
|
28
|
+
seconds, n = seconds.divmod(count)
|
29
|
+
"#{n.to_i} #{name}"
|
30
|
+
end
|
31
|
+
end.compact.reverse.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def start
|
35
|
+
@stopped_at = nil
|
36
|
+
@started_at = now
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
@total_elapsed += elapsed
|
41
|
+
@stopped_at = now
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset
|
45
|
+
@stopped_at = nil
|
46
|
+
@started_at = nil
|
47
|
+
@total_elapsed = 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def advance(milliseconds)
|
51
|
+
@total_elapsed += milliseconds
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def now
|
57
|
+
Time.now
|
58
|
+
end
|
59
|
+
|
60
|
+
def running?
|
61
|
+
!started_at.nil? && stopped_at.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
def looking_at
|
65
|
+
running? ? now : stopped_at
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/basher/ui.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'basher/ui/base_view'
|
2
|
+
require 'basher/ui/debug_view'
|
3
|
+
require 'basher/ui/loading_view'
|
4
|
+
require 'basher/ui/title_view'
|
5
|
+
require 'basher/ui/menu_view'
|
6
|
+
require 'basher/ui/info_view'
|
7
|
+
require 'basher/ui/current_word_view'
|
8
|
+
require 'basher/ui/remaining_words_view'
|
9
|
+
require 'basher/ui/progress_view'
|
10
|
+
require 'basher/ui/score_view'
|
11
|
+
|
12
|
+
module Basher
|
13
|
+
module UI
|
14
|
+
def views
|
15
|
+
@views ||= methods.grep(/(?<!base)_view/).map { |v| self.public_send(v) }.flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_views
|
19
|
+
views = case state.current
|
20
|
+
when :loading
|
21
|
+
[loading_view]
|
22
|
+
when :menu
|
23
|
+
[title_view, menu_view]
|
24
|
+
when :in_game
|
25
|
+
[info_view, current_word_view, remaining_words_view, progress_view]
|
26
|
+
when :score
|
27
|
+
[score_view]
|
28
|
+
when :paused
|
29
|
+
[title_view, menu_view]
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
|
34
|
+
views << debug_view if debugging?
|
35
|
+
|
36
|
+
views
|
37
|
+
end
|
38
|
+
|
39
|
+
def debug_view
|
40
|
+
@debug_view ||= DebugView.new do |v|
|
41
|
+
v.game = self
|
42
|
+
|
43
|
+
v.lines = DebugView.lines
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def loading_view
|
48
|
+
@loading_view ||= LoadingView.new do |v|
|
49
|
+
v.text = 'Loading...'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def title_view
|
54
|
+
@title_view ||= TitleView.new do |v|
|
55
|
+
v.state = state
|
56
|
+
|
57
|
+
v.lines = TitleView.lines
|
58
|
+
v.line = -> {
|
59
|
+
(v.parent.lines - TitleView.lines - MenuView.lines) / 2
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def menu_view
|
65
|
+
@menu_view ||= MenuView.new do |v|
|
66
|
+
v.state = state
|
67
|
+
|
68
|
+
v.lines = MenuView.lines
|
69
|
+
v.line = -> { v.parent.lines - MenuView.lines }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_word_view
|
74
|
+
@current_word_view ||= CurrentWordView.new do |v|
|
75
|
+
v.game = self
|
76
|
+
|
77
|
+
v.lines = CurrentWordView.lines
|
78
|
+
v.line = -> {
|
79
|
+
other = CurrentWordView.lines + InfoView.lines +
|
80
|
+
RemainingWordsView.lines + ProgressView.lines
|
81
|
+
|
82
|
+
(v.parent.lines - other) / 2
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def info_view
|
88
|
+
@info_view ||= InfoView.new do |v|
|
89
|
+
v.game = self
|
90
|
+
|
91
|
+
v.line = 0
|
92
|
+
v.lines = InfoView.lines
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def remaining_words_view
|
97
|
+
@remaining_words_view ||= RemainingWordsView.new do |v|
|
98
|
+
v.game = self
|
99
|
+
|
100
|
+
v.lines = RemainingWordsView.lines
|
101
|
+
v.line = -> { v.parent.lines - 2 }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def progress_view
|
106
|
+
@progress_view ||= ProgressView.new do |v|
|
107
|
+
v.game = self
|
108
|
+
|
109
|
+
v.lines = ProgressView.lines
|
110
|
+
v.line = -> { v.parent.lines - 1 }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def score_view
|
115
|
+
@score_view ||= ScoreView.new do |v|
|
116
|
+
v.game = self
|
117
|
+
|
118
|
+
v.lines = ScoreView.lines
|
119
|
+
v.line = -> {
|
120
|
+
(v.parent.lines - ScoreView.lines) / 2
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Basher
|
2
|
+
module UI
|
3
|
+
class BaseView < Curtis::View
|
4
|
+
|
5
|
+
attr_accessor :should_redraw
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.should_redraw = false
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def will_resize!
|
13
|
+
self.should_redraw = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def resize_and_reposition
|
17
|
+
reposition
|
18
|
+
resize
|
19
|
+
self.should_redraw = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear(also_thread = true)
|
23
|
+
clear_thread! if also_thread
|
24
|
+
window.clear
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|