adamsanderson-lexery 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +91 -0
- data/README.markdown +55 -0
- data/bin/lexery +5 -0
- data/fonts/MuseoSans_500.otf +0 -0
- data/fonts/license_agreement.rtf +46 -0
- data/images/cursor.png +0 -0
- data/lib/exuberant/abstract_screen.rb +37 -0
- data/lib/exuberant/button.rb +51 -0
- data/lib/exuberant/colored.rb +13 -0
- data/lib/exuberant/container.rb +31 -0
- data/lib/exuberant/exuberant_game.rb +28 -0
- data/lib/exuberant/fade.rb +39 -0
- data/lib/exuberant/game_window.rb +34 -0
- data/lib/exuberant/gosu_support.rb +7 -0
- data/lib/exuberant/label.rb +40 -0
- data/lib/exuberant/move.rb +37 -0
- data/lib/exuberant/multi_transition.rb +17 -0
- data/lib/exuberant/positioned.rb +30 -0
- data/lib/exuberant/publisher.rb +43 -0
- data/lib/exuberant/support.rb +42 -0
- data/lib/exuberant/timer.rb +30 -0
- data/lib/exuberant/transition.rb +71 -0
- data/lib/lexery.rb +11 -0
- data/lib/lexery/colors.rb +9 -0
- data/lib/lexery/dictionary.rb +14 -0
- data/lib/lexery/fading_message.rb +22 -0
- data/lib/lexery/fps_counter.rb +19 -0
- data/lib/lexery/game.rb +32 -0
- data/lib/lexery/game_over_screen.rb +39 -0
- data/lib/lexery/game_rules.rb +51 -0
- data/lib/lexery/game_screen.rb +128 -0
- data/lib/lexery/layers.rb +5 -0
- data/lib/lexery/round.rb +17 -0
- data/lib/lexery/score_board.rb +31 -0
- data/lib/lexery/stats_screen.rb +46 -0
- data/lib/lexery/title_screen.rb +83 -0
- data/lib/lexery/word_control.rb +72 -0
- data/options.yml +9 -0
- data/script/console +5 -0
- data/sounds/README +6 -0
- data/sounds/bell.wav +0 -0
- data/sounds/chick.wav +0 -0
- data/sounds/click.wav +0 -0
- data/wordlists/README +11 -0
- data/wordlists/words.set +3611 -0
- data/wordlists/words.txt +178691 -0
- metadata +107 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
class Label
|
2
|
+
attr_accessor :x, :y, :width, :height, :text, :color
|
3
|
+
include Positioned
|
4
|
+
|
5
|
+
def initialize(x,y, text=nil, options={}, &block)
|
6
|
+
super()
|
7
|
+
if text.is_a?(Hash)
|
8
|
+
options = text
|
9
|
+
text = ''
|
10
|
+
end
|
11
|
+
|
12
|
+
@x = x
|
13
|
+
@y = y
|
14
|
+
@options = options
|
15
|
+
|
16
|
+
@height = options[:height] || 32
|
17
|
+
@font = options[:font] || Game.load_font(:default, @height - 4)
|
18
|
+
@width = options[:width] || @font.text_width(@text) + 4
|
19
|
+
|
20
|
+
self.text = text
|
21
|
+
@block = block if block_given?
|
22
|
+
|
23
|
+
@color = options[:color] || Gosu::Color.new(0x66666666)
|
24
|
+
end
|
25
|
+
|
26
|
+
def text= new_text
|
27
|
+
if new_text != @text
|
28
|
+
@text = new_text
|
29
|
+
@width = @font.text_width(@text) + 4 unless @options[:width]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def update
|
34
|
+
self.text = @block.call if @block
|
35
|
+
end
|
36
|
+
|
37
|
+
def draw
|
38
|
+
@font.draw(text, x , y , Layers::UI, 1, 1, @color, :default)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Move < MultiTransition
|
2
|
+
class << self
|
3
|
+
# Shorthand to create and start a move effect
|
4
|
+
def [] object, options={}
|
5
|
+
|
6
|
+
if options[:to]
|
7
|
+
x,y = options[:to]
|
8
|
+
elsif options[:by]
|
9
|
+
x,y = options[:by]
|
10
|
+
x += object.x
|
11
|
+
y += object.y
|
12
|
+
else
|
13
|
+
raise ArgumentError,"must specify :to, or :by as an option"
|
14
|
+
end
|
15
|
+
|
16
|
+
duration = options[:in] || 1000
|
17
|
+
|
18
|
+
Game.state.add self.new(object, x,y, duration, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def insert object, options={}
|
22
|
+
options[:before] = lambda{ Game.state.add(object) }.compose(options[:before])
|
23
|
+
self[object, options]
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove object, options={}
|
27
|
+
options[:after] = lambda{ Game.state.remove(object) }.compose(options[:after])
|
28
|
+
self[object, options]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(object, dest_x,dest_y, duration=1000, options={})
|
33
|
+
super(duration, [object.x, object.y], [dest_x,dest_y], options ){|x,y|
|
34
|
+
object.x, object.y = x,y
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Operates on multiple attributes at once
|
2
|
+
class MultiTransition < Transition
|
3
|
+
# Initialize with multiple start and end values, store them as a vector
|
4
|
+
def initialize(duration, start_values, end_values, options={}, &action)
|
5
|
+
super(duration, Vector[*start_values], Vector[*end_values], options, &action)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Compute the change for each value
|
9
|
+
def compute_change(start_values, end_values)
|
10
|
+
(end_values - start_values).map{|v| v.to_f}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Same as the normal apply, but unpack the vector
|
14
|
+
def apply(t)
|
15
|
+
@action.call(* tweening_function[t, @start_value, @change, @duration])
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Expects x,y,width,height
|
2
|
+
module Positioned
|
3
|
+
def mouse_within?
|
4
|
+
within? Game.window.mouse_x, Game.window.mouse_y
|
5
|
+
end
|
6
|
+
|
7
|
+
def top
|
8
|
+
y
|
9
|
+
end
|
10
|
+
|
11
|
+
def bottom
|
12
|
+
y + height
|
13
|
+
end
|
14
|
+
|
15
|
+
def left
|
16
|
+
x
|
17
|
+
end
|
18
|
+
|
19
|
+
def right
|
20
|
+
x + width
|
21
|
+
end
|
22
|
+
|
23
|
+
def within?(x,y)
|
24
|
+
left <= x and
|
25
|
+
top <= y and
|
26
|
+
right > x and
|
27
|
+
bottom > y
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Publisher
|
2
|
+
Subscriber = Struct.new :subscriber, :block
|
3
|
+
|
4
|
+
def subscribers
|
5
|
+
@subscribers ||= Hash.new{|h,k| h[k] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
def subscribe(event, subscriber=nil, &block)
|
9
|
+
validate_event! event
|
10
|
+
subscribers[event] << Subscriber.new(subscriber, block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def unsubscribe(event, subscriber=nil)
|
14
|
+
validate_event! event
|
15
|
+
subscribers[event].delete_if{|s| s.subscriber == subscriber}
|
16
|
+
end
|
17
|
+
|
18
|
+
def fire_event(event, *args)
|
19
|
+
validate_event! event
|
20
|
+
subscribers[event].each{|sub| sub.block.call(*args)}
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_event!(event)
|
24
|
+
raise "No such event '#{event}', should be one of #{self.class.events.inspect}" unless self.class.events.include? event
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.included(base)
|
28
|
+
base.extend(ClassMethods)
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def events(*events)
|
33
|
+
@pub_sub_events ||= []
|
34
|
+
if events.empty?
|
35
|
+
inherited_events = superclass.respond_to?(:events) ? superclass.send(:events) : []
|
36
|
+
[@pub_sub_events, inherited_events].flatten
|
37
|
+
else
|
38
|
+
@pub_sub_events.insert(0,*events)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Ensure that classes are loaded on demand.
|
2
|
+
class Module
|
3
|
+
def const_missing(sym)
|
4
|
+
require sym.to_s.underscore
|
5
|
+
c = const_get(sym)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# String Helper, from ActiveSupport
|
10
|
+
class String
|
11
|
+
def underscore
|
12
|
+
self.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
13
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
14
|
+
tr("-", "_").
|
15
|
+
downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(index)
|
19
|
+
chars[index]
|
20
|
+
end
|
21
|
+
|
22
|
+
def chars
|
23
|
+
self.split(//)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Range
|
28
|
+
def [] index
|
29
|
+
raise ArgumentError.new("Index #{index} must be between 0 and 1") if (index > 1 || index < 0)
|
30
|
+
(self.end - self.begin) * index + self.begin
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Proc
|
35
|
+
def compose(g)
|
36
|
+
return self if g.nil?
|
37
|
+
lambda { |*args| self[g[*args]] }
|
38
|
+
end
|
39
|
+
def *(g)
|
40
|
+
compose(self, g)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Timer
|
2
|
+
include Publisher
|
3
|
+
events :ticked
|
4
|
+
attr_reader :ticks
|
5
|
+
|
6
|
+
def initialize(period, &action)
|
7
|
+
@period = period
|
8
|
+
@ticks = nil
|
9
|
+
subscribe(:ticked, self, &action) if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
@started = Gosu::milliseconds
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
@started = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def update
|
21
|
+
if @started
|
22
|
+
current_ticks = (Gosu::milliseconds - @started) / @period
|
23
|
+
if !@ticks || current_ticks > @ticks
|
24
|
+
@ticks = current_ticks
|
25
|
+
fire_event :ticked, current_ticks
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class Transition
|
2
|
+
def initialize(duration, start_value, end_value, options={}, &action)
|
3
|
+
@duration = duration.to_f
|
4
|
+
@start_value = start_value
|
5
|
+
@change = compute_change(start_value, end_value)
|
6
|
+
@options = options
|
7
|
+
|
8
|
+
@action = action
|
9
|
+
|
10
|
+
delay = options[:delay] || 0
|
11
|
+
|
12
|
+
before
|
13
|
+
@starts = Gosu::milliseconds + delay
|
14
|
+
end
|
15
|
+
|
16
|
+
def before
|
17
|
+
@options[:before].call if @options[:before]
|
18
|
+
end
|
19
|
+
|
20
|
+
def after
|
21
|
+
@options[:after].call if @options[:after]
|
22
|
+
Game.state.remove self
|
23
|
+
end
|
24
|
+
|
25
|
+
def tweening_function
|
26
|
+
return @function if @function
|
27
|
+
|
28
|
+
if @options[:mode].is_a? Symbol
|
29
|
+
@function = TWEEN_FUNCTIONS[@options[:mode]]
|
30
|
+
else
|
31
|
+
@function = @options[:mode] || TWEEN_FUNCTIONS.default
|
32
|
+
end
|
33
|
+
|
34
|
+
@function
|
35
|
+
end
|
36
|
+
|
37
|
+
def compute_change(start_value, end_value)
|
38
|
+
start_value.to_f - end_value.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def update
|
42
|
+
t = (Gosu::milliseconds - @starts)
|
43
|
+
|
44
|
+
if t >= @duration
|
45
|
+
after
|
46
|
+
elsif t >= 0
|
47
|
+
apply(t)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply(t)
|
52
|
+
@action.call(tweening_function[t, @start_value, @change, @duration])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Tweening functions; as per Robert Penner
|
56
|
+
# http://www.robertpenner.com/easing/penner_chapter7_tweening.pdf
|
57
|
+
# t: time
|
58
|
+
# b: beginning
|
59
|
+
# c: change
|
60
|
+
# d: duration
|
61
|
+
TWEEN_FUNCTIONS={
|
62
|
+
:linear_tween => lambda{|t, b, c, d| c*(t/d) + b},
|
63
|
+
:ease_in_quad => lambda{|t, b, c, d| c*(t/=d)*t + b},
|
64
|
+
:ease_out_quad => lambda{|t, b, c, d| c * (t/=d)*(t-2)*-1 + b},
|
65
|
+
:ease_in_quart => lambda{|t, b, c, d| c * ((t/d) ** 4) + b},
|
66
|
+
:ease_out_quart =>lambda{|t, b, c, d| c * ((t/d-1) ** 4)*-1 -1 + b},
|
67
|
+
:ease_in_sine => lambda{|t, b, c, d| c * (1 - Math.cos(t/d * (Math::PI/2))) + b},
|
68
|
+
:ease_out_sine => lambda{|t, b, c, d| c * Math.sin(t/d * (Math::PI/2)) + b}
|
69
|
+
}
|
70
|
+
TWEEN_FUNCTIONS.default = TWEEN_FUNCTIONS[:linear_tween]
|
71
|
+
end
|
data/lib/lexery.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
GAME_ROOT = File.join(File.dirname(__FILE__),'..')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'gosu'
|
5
|
+
require 'matrix' # used in multi_transition
|
6
|
+
require 'set'
|
7
|
+
|
8
|
+
$LOAD_PATH << File.join(GAME_ROOT,'lib','lexery')
|
9
|
+
$LOAD_PATH << File.join(GAME_ROOT,'lib','exuberant')
|
10
|
+
require 'support'
|
11
|
+
require 'gosu_support'
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Colors
|
2
|
+
HEADER = Gosu::Color.new(0x666666FF)
|
3
|
+
WARNING = Gosu::Color.new(128, 255,0,0)
|
4
|
+
FADED = Gosu::Color.new(96, 128, 128, 128)
|
5
|
+
FADED_WARNING = Gosu::Color.new(96, 128, 0, 0)
|
6
|
+
|
7
|
+
BACKGROUND_TOP = Gosu::Color.new(255, 255, 255, 255)
|
8
|
+
BACKGROUND_BOTTOM = Gosu::Color.new(255, 200, 200, 255)
|
9
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Dictionary
|
2
|
+
def initialize(path)
|
3
|
+
@words = Marshal.load(File.read(path))
|
4
|
+
end
|
5
|
+
|
6
|
+
def valid_word?(word)
|
7
|
+
@words.include? word.downcase
|
8
|
+
end
|
9
|
+
|
10
|
+
def pick(length=5)
|
11
|
+
candidates = @words.select{|w| w.length == length }
|
12
|
+
candidates[ rand(candidates.length) ]
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class FadingMessage < Label
|
2
|
+
def initialize(x,y, text, options={})
|
3
|
+
super(x,y, text, options)
|
4
|
+
|
5
|
+
@start_y = self.y
|
6
|
+
@target_y = 0 - self.height
|
7
|
+
@base_color = self.color
|
8
|
+
|
9
|
+
Game.state.add MultiTransition.new(3000, [@start_y, 255], [@target_y, 0],
|
10
|
+
:mode=>options[:mode],
|
11
|
+
:after=>lambda{Game.state.remove self}
|
12
|
+
){|y, alpha|
|
13
|
+
self.color = Gosu::Color.new(
|
14
|
+
alpha.to_i,
|
15
|
+
@base_color.red,
|
16
|
+
@base_color.green,
|
17
|
+
@base_color.blue
|
18
|
+
)
|
19
|
+
self.y = y
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class FpsCounter
|
2
|
+
attr_reader :fps
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@current_second = Gosu::milliseconds / 1000
|
6
|
+
@accum_fps = 0
|
7
|
+
@fps = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_tick
|
11
|
+
@accum_fps += 1
|
12
|
+
current_second = Gosu::milliseconds / 1000
|
13
|
+
if current_second != @current_second
|
14
|
+
@current_second = current_second
|
15
|
+
@fps = @accum_fps
|
16
|
+
@accum_fps = 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/lexery/game.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
class Game < ExuberantGame
|
2
|
+
class << self
|
3
|
+
def options_set
|
4
|
+
@options_set ||= 'short game'
|
5
|
+
end
|
6
|
+
|
7
|
+
def all_options
|
8
|
+
@options ||= YAML.load_file(File.join(GAME_ROOT, 'options.yml'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
all_options[options_set]
|
13
|
+
end
|
14
|
+
|
15
|
+
def dictionary
|
16
|
+
@dictionary ||= begin
|
17
|
+
path = File.join(GAME_ROOT, 'wordlists', 'words.set')
|
18
|
+
Dictionary.new(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def score_board
|
23
|
+
@score_board ||= begin
|
24
|
+
locations = [File.join(GAME_ROOT, 'scores'), File.join(ENV['HOME'],'.lexery_scores')]
|
25
|
+
# find a place where we can write the scores to
|
26
|
+
path = locations.find{|p| File.writable?(File.dirname(p))}
|
27
|
+
ScoreBoard.new(path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|