adamsanderson-lexery 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/LICENSE +91 -0
  2. data/README.markdown +55 -0
  3. data/bin/lexery +5 -0
  4. data/fonts/MuseoSans_500.otf +0 -0
  5. data/fonts/license_agreement.rtf +46 -0
  6. data/images/cursor.png +0 -0
  7. data/lib/exuberant/abstract_screen.rb +37 -0
  8. data/lib/exuberant/button.rb +51 -0
  9. data/lib/exuberant/colored.rb +13 -0
  10. data/lib/exuberant/container.rb +31 -0
  11. data/lib/exuberant/exuberant_game.rb +28 -0
  12. data/lib/exuberant/fade.rb +39 -0
  13. data/lib/exuberant/game_window.rb +34 -0
  14. data/lib/exuberant/gosu_support.rb +7 -0
  15. data/lib/exuberant/label.rb +40 -0
  16. data/lib/exuberant/move.rb +37 -0
  17. data/lib/exuberant/multi_transition.rb +17 -0
  18. data/lib/exuberant/positioned.rb +30 -0
  19. data/lib/exuberant/publisher.rb +43 -0
  20. data/lib/exuberant/support.rb +42 -0
  21. data/lib/exuberant/timer.rb +30 -0
  22. data/lib/exuberant/transition.rb +71 -0
  23. data/lib/lexery.rb +11 -0
  24. data/lib/lexery/colors.rb +9 -0
  25. data/lib/lexery/dictionary.rb +14 -0
  26. data/lib/lexery/fading_message.rb +22 -0
  27. data/lib/lexery/fps_counter.rb +19 -0
  28. data/lib/lexery/game.rb +32 -0
  29. data/lib/lexery/game_over_screen.rb +39 -0
  30. data/lib/lexery/game_rules.rb +51 -0
  31. data/lib/lexery/game_screen.rb +128 -0
  32. data/lib/lexery/layers.rb +5 -0
  33. data/lib/lexery/round.rb +17 -0
  34. data/lib/lexery/score_board.rb +31 -0
  35. data/lib/lexery/stats_screen.rb +46 -0
  36. data/lib/lexery/title_screen.rb +83 -0
  37. data/lib/lexery/word_control.rb +72 -0
  38. data/options.yml +9 -0
  39. data/script/console +5 -0
  40. data/sounds/README +6 -0
  41. data/sounds/bell.wav +0 -0
  42. data/sounds/chick.wav +0 -0
  43. data/sounds/click.wav +0 -0
  44. data/wordlists/README +11 -0
  45. data/wordlists/words.set +3611 -0
  46. data/wordlists/words.txt +178691 -0
  47. 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
@@ -0,0 +1,5 @@
1
+ module Layers
2
+ CURSOR = 1500
3
+ UI = 1000
4
+ BACKGROUND = 200
5
+ end
@@ -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