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