basher-basher 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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