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,39 @@
|
|
1
|
+
class GameOverScreen < AbstractScreen
|
2
|
+
def initialize(round)
|
3
|
+
super()
|
4
|
+
|
5
|
+
words = round.words
|
6
|
+
imaginary_words = round.imaginary_words
|
7
|
+
|
8
|
+
add title = Label.new(10, 10, 'Game Over', :color=>Colors::HEADER, :height=>48)
|
9
|
+
title.x = Game.window.width/2 - title.width/2
|
10
|
+
|
11
|
+
add score = Label.new(320, 256, "'#{round.initial_word}' #{round.score} words")
|
12
|
+
add timer = Timer.new(1000){|ticks|
|
13
|
+
word = words[(ticks+1) % words.length]
|
14
|
+
color = imaginary_words.include?(word) ? Colors::FADED_WARNING : Colors::FADED
|
15
|
+
add FadingMessage.new(320, score.top-32, word, :color=>color)
|
16
|
+
}
|
17
|
+
|
18
|
+
add new_game = Button.new(score.left, score.bottom + 4, "New Game"){
|
19
|
+
window.next_state = GameScreen.new()
|
20
|
+
}
|
21
|
+
|
22
|
+
add replay_game = Button.new(new_game.left, new_game.bottom + 4, "Try Again"){
|
23
|
+
window.next_state = GameScreen.new(round.initial_word)
|
24
|
+
}
|
25
|
+
|
26
|
+
add Button.new(10, height - 32, "Back"){
|
27
|
+
window.next_state = TitleScreen.new
|
28
|
+
}
|
29
|
+
|
30
|
+
timer.start
|
31
|
+
end
|
32
|
+
|
33
|
+
def button_down(id)
|
34
|
+
case id
|
35
|
+
when Gosu::KbEscape then
|
36
|
+
window.next_state = TitleScreen.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class GameRules
|
2
|
+
def valid_transition?(old_word, new_word)
|
3
|
+
d = distance(old_word, new_word)
|
4
|
+
case d
|
5
|
+
when 1 then true
|
6
|
+
when 2 then
|
7
|
+
old_word.chars.sort == new_word.chars.sort
|
8
|
+
else
|
9
|
+
false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
# From:
|
15
|
+
# http://rubyforge.org/projects/text
|
16
|
+
def distance(str1, str2)
|
17
|
+
if $KCODE =~ /^U/i
|
18
|
+
unpack_rule = 'U*'
|
19
|
+
else
|
20
|
+
unpack_rule = 'C*'
|
21
|
+
end
|
22
|
+
s = str1.unpack(unpack_rule)
|
23
|
+
t = str2.unpack(unpack_rule)
|
24
|
+
n = s.length
|
25
|
+
m = t.length
|
26
|
+
return m if (0 == n)
|
27
|
+
return n if (0 == m)
|
28
|
+
|
29
|
+
d = (0..m).to_a
|
30
|
+
x = nil
|
31
|
+
|
32
|
+
(0...n).each do |i|
|
33
|
+
e = i+1
|
34
|
+
(0...m).each do |j|
|
35
|
+
cost = (s[i] == t[j]) ? 0 : 1
|
36
|
+
x = [
|
37
|
+
d[j+1] + 1, # insertion
|
38
|
+
e + 1, # deletion
|
39
|
+
d[j] + cost # substitution
|
40
|
+
].min
|
41
|
+
d[j] = e
|
42
|
+
e = x
|
43
|
+
end
|
44
|
+
d[m] = x
|
45
|
+
end
|
46
|
+
|
47
|
+
return x
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
class GameScreen < AbstractScreen
|
2
|
+
def initialize(word=nil)
|
3
|
+
super()
|
4
|
+
|
5
|
+
@rules = GameRules.new
|
6
|
+
@initial_word = word || Game.dictionary.pick
|
7
|
+
@imaginary_count = Game.options['imaginary_words']
|
8
|
+
@words = []
|
9
|
+
@imaginary_words = []
|
10
|
+
@duration = Game.options['duration']
|
11
|
+
|
12
|
+
@click_sound = Game.load_sound 'click.wav'
|
13
|
+
@done_sound = Game.load_sound 'bell.wav'
|
14
|
+
@accept_sound = Game.load_sound 'chick.wav'
|
15
|
+
|
16
|
+
add @status = Label.new(10, self.height - 32, "current word: #{word}", :height=>32)
|
17
|
+
add score = Label.new(10, 10, :height=>16){"words: #{@words.length}"}
|
18
|
+
add Label.new(10, score.bottom + 2, :height=>16){ "imaginary words remaining: #{@imaginary_count}" }
|
19
|
+
|
20
|
+
if @duration
|
21
|
+
add remaining_label = Label.new(self.width - 64, 10, @duration)
|
22
|
+
add timer = Timer.new(1000){|ticks|
|
23
|
+
remaining = @duration - ticks
|
24
|
+
remaining_label.text = remaining
|
25
|
+
case remaining
|
26
|
+
when @duration: message "Welcome"
|
27
|
+
when 10: remaining_label.color = Colors::WARNING
|
28
|
+
when 5: message "5 seconds remaining", :color=>Colors::WARNING
|
29
|
+
@click_sound.play
|
30
|
+
when 4,3,2,1: message "#{remaining}", :color=>Colors::WARNING
|
31
|
+
@click_sound.play
|
32
|
+
when 0: game_over
|
33
|
+
@done_sound.play
|
34
|
+
end
|
35
|
+
}
|
36
|
+
timer.start
|
37
|
+
end
|
38
|
+
|
39
|
+
add @word_control = WordControl.new(320, 256, @initial_word)
|
40
|
+
|
41
|
+
add @reset_button = Button.new(@word_control.left, @word_control.bottom + 10, 'reset'){
|
42
|
+
@word_control.reset
|
43
|
+
}
|
44
|
+
|
45
|
+
add @ok_button = Button.new(@reset_button.right + 12, @word_control.bottom + 10, 'ok'){ accept }
|
46
|
+
add @done_button = Button.new(width - 64, @status.top, 'done'){ game_over }
|
47
|
+
|
48
|
+
@started = Time.now
|
49
|
+
end
|
50
|
+
|
51
|
+
def update
|
52
|
+
super
|
53
|
+
|
54
|
+
# todo: make this into an event/listener
|
55
|
+
# todo: make this into the rules object
|
56
|
+
unless @last_word == @word_control.text
|
57
|
+
@valid_transition = @rules.valid_transition? @word_control.word, @word_control.text
|
58
|
+
@valid_word = Game.dictionary.valid_word? @word_control.text
|
59
|
+
@last_word = @word_control.text
|
60
|
+
@changed_word = @word_control.word != @word_control.text
|
61
|
+
@new_word = !@words.include?(@word_control.text)
|
62
|
+
|
63
|
+
@word_control.valid = @valid_transition && @valid_word && @new_word && @changed_word
|
64
|
+
@imaginary_word = @valid_transition && @new_word && @changed_word && !@valid_word
|
65
|
+
|
66
|
+
@status.text = if !@changed_word
|
67
|
+
"Add, remove, or change one letter to create a new word"
|
68
|
+
elsif !@valid_transition
|
69
|
+
"You may only add, remove, or change one letter"
|
70
|
+
elsif !@valid_word
|
71
|
+
"'#{@word_control.text}' is not in the dictionary"
|
72
|
+
elsif !@new_word
|
73
|
+
"'#{@word_control.text}' has already been used"
|
74
|
+
else
|
75
|
+
"Click 'ok', or hit 'enter' to use this word"
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def accept
|
82
|
+
if @word_control.valid || (@imaginary_word && @imaginary_count > 0)
|
83
|
+
word = @word_control.text
|
84
|
+
@accept_sound.play
|
85
|
+
|
86
|
+
if @imaginary_word
|
87
|
+
@imaginary_words << word
|
88
|
+
@imaginary_count -= 1
|
89
|
+
end
|
90
|
+
|
91
|
+
@words << word
|
92
|
+
message word, :color=>(@imaginary_word ? Gosu::Color.new(128, 255,0,0) : nil )
|
93
|
+
@word_control.word = word
|
94
|
+
else
|
95
|
+
#@reject_sound.play
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def game_over
|
100
|
+
score = [0, @words.length - @imaginary_words.length].max
|
101
|
+
round = Round.new(
|
102
|
+
:started => @started,
|
103
|
+
:initial_word => @initial_word,
|
104
|
+
:words => @words,
|
105
|
+
:imaginary_words => @imaginary_words
|
106
|
+
)
|
107
|
+
|
108
|
+
Game.score_board.record(Game.options_set, round)
|
109
|
+
window.next_state = GameOverScreen.new(round)
|
110
|
+
end
|
111
|
+
|
112
|
+
def button_down(id)
|
113
|
+
super
|
114
|
+
|
115
|
+
case id
|
116
|
+
when Gosu::KbReturn, Gosu::KbEnter then
|
117
|
+
accept
|
118
|
+
when Gosu::KbTab then
|
119
|
+
@word_control.reset
|
120
|
+
when Gosu::KbEscape then
|
121
|
+
window.next_state = TitleScreen.new
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def message(text, options={})
|
126
|
+
add m = FadingMessage.new(320, 224, text, options)
|
127
|
+
end
|
128
|
+
end
|
data/lib/lexery/round.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Round
|
2
|
+
attr_reader :started, :words, :imaginary_words
|
3
|
+
def initialize(hash)
|
4
|
+
@started = hash[:started]
|
5
|
+
@score = hash[:score]
|
6
|
+
@words = hash[:words]
|
7
|
+
@imaginary_words = hash[:imaginary_words]
|
8
|
+
end
|
9
|
+
|
10
|
+
def score
|
11
|
+
@words.length
|
12
|
+
end
|
13
|
+
|
14
|
+
def initial_word
|
15
|
+
words.first
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'round'
|
2
|
+
|
3
|
+
class ScoreBoard
|
4
|
+
def initialize(path)
|
5
|
+
@path = path
|
6
|
+
if path and File.exist?(path)
|
7
|
+
@games = Marshal.load(File.read(path))
|
8
|
+
else
|
9
|
+
@games = {}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def rounds(type)
|
14
|
+
@games[type] ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def record(type, round)
|
18
|
+
raise ArgumentError unless round.is_a? Round
|
19
|
+
|
20
|
+
rounds(type) << round
|
21
|
+
save!
|
22
|
+
end
|
23
|
+
|
24
|
+
def save!
|
25
|
+
return unless @path
|
26
|
+
|
27
|
+
File.open(@path,'w') do |io|
|
28
|
+
Marshal.dump(@games,io)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class StatsScreen < AbstractScreen
|
2
|
+
def initialize
|
3
|
+
super
|
4
|
+
add title = Label.new(10, 10, 'Lexery Stats', :color=>Colors::HEADER, :height=>48)
|
5
|
+
title.x = Game.window.width/2 - title.width/2
|
6
|
+
|
7
|
+
labels = []
|
8
|
+
Game.all_options.each_with_index{|option, i|
|
9
|
+
key = option.first
|
10
|
+
y = 128+i*96
|
11
|
+
add label = Label.new(320, y, key.capitalize)
|
12
|
+
games = Game.score_board.rounds(key)
|
13
|
+
count = games.length
|
14
|
+
|
15
|
+
if count > 0
|
16
|
+
scores = games.select{|g| g.score > 0}.map{|g| g.score}
|
17
|
+
average = scores.inject{|m,v| m+v} / scores.length.to_f # TODO: need some stats helpers
|
18
|
+
best_round = games.sort_by{|g| [-g.score, -g.started.to_i]}.first
|
19
|
+
|
20
|
+
add label = Label.new(340, y+36, "Rounds Played", :height=>18)
|
21
|
+
add label = Label.new(480, y+36, count, :height=>18)
|
22
|
+
add label = Label.new(340, y+50, "Average Score", :height=>18)
|
23
|
+
add label = Label.new(480, y+50, format('%.2f',average), :height=>18)
|
24
|
+
add label = Label.new(340, y+66, "Best Round", :height=>18)
|
25
|
+
add label = Label.new(480, y+66, "'#{best_round.initial_word}' #{best_round.score}", :height=>18)
|
26
|
+
else
|
27
|
+
add label = Label.new(340, y+36, "No games played yet", :height=>18)
|
28
|
+
end
|
29
|
+
labels << label
|
30
|
+
}
|
31
|
+
|
32
|
+
add Button.new(10, height - 32, "Back"){
|
33
|
+
window.next_state = TitleScreen.new
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def button_down(id)
|
38
|
+
super
|
39
|
+
|
40
|
+
case id
|
41
|
+
when Gosu::KbEscape then
|
42
|
+
window.next_state = TitleScreen.new
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class TitleScreen < AbstractScreen
|
2
|
+
def initialize
|
3
|
+
super
|
4
|
+
add title = Label.new(10, 10, 'Lexery', :color=>Colors::HEADER, :height=>48)
|
5
|
+
title.x = Game.window.width/2 - title.width/2
|
6
|
+
|
7
|
+
add @word = Label.new(320, 200, '', :color=>Colors::HEADER, :height=>36)
|
8
|
+
|
9
|
+
add timer = Timer.new(4000){|ticks|
|
10
|
+
case ticks % 8
|
11
|
+
when 0: message "Make Words Into New Words"
|
12
|
+
when 1: message "Start with Grass"
|
13
|
+
word "Grass"
|
14
|
+
when 2: message "Replace the r with l"
|
15
|
+
word "Glass"
|
16
|
+
when 3: message "Remove the G"
|
17
|
+
word "Lass"
|
18
|
+
when 4: message "Replace the s with t"
|
19
|
+
word "Last"
|
20
|
+
when 5: message "Swap the L and s"
|
21
|
+
word "Salt"
|
22
|
+
when 6: message "Add an S"
|
23
|
+
word "Salts"
|
24
|
+
when 7: message "That's all there is to it"
|
25
|
+
word ""
|
26
|
+
end
|
27
|
+
}
|
28
|
+
timer.start
|
29
|
+
|
30
|
+
start_buttons = []
|
31
|
+
Game.all_options.each_with_index{|option, i|
|
32
|
+
key = option.first
|
33
|
+
add button = Button.new(320, 256+i*42, key.capitalize){
|
34
|
+
Game.options_set = key
|
35
|
+
window.next_state = GameScreen.new
|
36
|
+
}
|
37
|
+
start_buttons << button
|
38
|
+
}
|
39
|
+
|
40
|
+
add Button.new(start_buttons.last.left, start_buttons.last.bottom + 32, "Statistics"){
|
41
|
+
window.next_state = StatsScreen.new
|
42
|
+
}
|
43
|
+
|
44
|
+
add Button.new(10, height - 32, "Quit"){
|
45
|
+
window.close
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def draw
|
50
|
+
super
|
51
|
+
# Draw the decoration around the instructions:
|
52
|
+
top = @word.top - @word.height * 1.4
|
53
|
+
bottom = @word.bottom + @word.height * 0.2
|
54
|
+
window.draw_quad 0, top, 0x66ffffff,
|
55
|
+
width, top, 0x66ffffff,
|
56
|
+
0, bottom, 0x66ffffff,
|
57
|
+
width, bottom, 0x66ffffff
|
58
|
+
window.draw_line 0, top, 0xccccccff,
|
59
|
+
width, top, 0xccccccff
|
60
|
+
window.draw_line 0, bottom, 0xccccccff,
|
61
|
+
width, bottom, 0xccccccff
|
62
|
+
end
|
63
|
+
|
64
|
+
def button_down(id)
|
65
|
+
case id
|
66
|
+
when Gosu::KbEscape: window.close
|
67
|
+
when Gosu::KbQ: window.close
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def message(text, options={})
|
72
|
+
label = Label.new(@word.left, @word.top - @word.height * 1.2, text)
|
73
|
+
Move.insert(label, :to=>[label.x, -label.height], :delay=>3000, :in=>1000, :mode=>:ease_in_quad, :after=>lambda{remove label})
|
74
|
+
Fade[label, {:to=>:out, :delay=>2500, :mode=>:ease_in_quad}]
|
75
|
+
end
|
76
|
+
|
77
|
+
def word(text)
|
78
|
+
Fade[@word, {
|
79
|
+
:to=>:out, :in=>500, :delay=>1000,
|
80
|
+
:after=>lambda{@word.text = text; Fade[@word, {:to=>:in}]}
|
81
|
+
}]
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class WordControl < Gosu::TextInput
|
2
|
+
attr_accessor :x, :y
|
3
|
+
attr_accessor :word, :width, :height
|
4
|
+
attr_reader :command
|
5
|
+
attr_accessor :valid
|
6
|
+
|
7
|
+
include Positioned
|
8
|
+
|
9
|
+
CARET_COLOR = Gosu::Color.new 0x996666FF
|
10
|
+
TEXT_COLOR = Gosu::Color.new 0x996666FF
|
11
|
+
VALID_COLOR = Gosu::Color.new 0xCC66DD66
|
12
|
+
INVALID_COLOR = Gosu::Color.new 0xCCFF6666
|
13
|
+
SELECTION_COLOR = Gosu::Color.new 0x330000FF
|
14
|
+
|
15
|
+
def initialize(x,y,word)
|
16
|
+
super()
|
17
|
+
|
18
|
+
@height = 64
|
19
|
+
@font = Game.load_font :default, @height
|
20
|
+
self.word = word
|
21
|
+
self.text = word
|
22
|
+
|
23
|
+
@x = x
|
24
|
+
@y = y
|
25
|
+
|
26
|
+
Game.window.text_input = self
|
27
|
+
end
|
28
|
+
|
29
|
+
def text
|
30
|
+
(super || '').downcase
|
31
|
+
end
|
32
|
+
|
33
|
+
def word= new_word
|
34
|
+
@width = @font.text_width(new_word)
|
35
|
+
@word = new_word
|
36
|
+
@valid = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset
|
40
|
+
self.text = @word
|
41
|
+
end
|
42
|
+
|
43
|
+
def update
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw
|
48
|
+
# Calculate the position of the caret and the selection start.
|
49
|
+
pos_x = x + @font.text_width(self.text[0...self.caret_pos])
|
50
|
+
sel_x = x + @font.text_width(self.text[0...self.selection_start])
|
51
|
+
|
52
|
+
# Draw the selection background, if any; if not, sel_x and pos_x will be
|
53
|
+
# the same value, making this quad empty.
|
54
|
+
Game.window.draw_quad(sel_x, y, SELECTION_COLOR,
|
55
|
+
pos_x, y, SELECTION_COLOR,
|
56
|
+
sel_x, y + height, SELECTION_COLOR,
|
57
|
+
pos_x, y + height, SELECTION_COLOR,
|
58
|
+
Layers::UI)
|
59
|
+
|
60
|
+
Game.window.draw_line(pos_x, y, CARET_COLOR,
|
61
|
+
pos_x, y + height, CARET_COLOR,
|
62
|
+
Layers::UI)
|
63
|
+
|
64
|
+
if self.word == self.text
|
65
|
+
color = TEXT_COLOR
|
66
|
+
else
|
67
|
+
color = valid ? VALID_COLOR : INVALID_COLOR
|
68
|
+
end
|
69
|
+
|
70
|
+
@font.draw(self.text, x , y , Layers::UI, 1, 1, color, :default)
|
71
|
+
end
|
72
|
+
end
|