nerdword 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ class Board
2
+ def initialize(values, letter_multipliers = {}, word_multipliers = {})
3
+ @values = values
4
+ @letter_multipliers = letter_multipliers
5
+ @word_multipliers = word_multipliers
6
+ @history = []
7
+ end
8
+
9
+ def play(move)
10
+ index = generate_index(@history)
11
+ moves = find_moves(move, index)
12
+ @history << move
13
+ score = moves.inject(0) { |score, move| score + score_move(move, index) }
14
+ tiles_used = calculate_tiles_used(move, index)
15
+ if tiles_used.length == 7
16
+ score += 50
17
+ end
18
+ [score, tiles_used]
19
+ end
20
+
21
+ private
22
+
23
+ def find_moves(move, index)
24
+ moves = [move]
25
+
26
+ direction = move.direction
27
+ orthogonal_direction = Direction.opposite(direction)
28
+
29
+ move.word.length.times do |i|
30
+ current = move.position.shift(i, direction)
31
+ next if index[current] # not placed by us, so don't look for adjacent letters
32
+
33
+ behind = current.previous(orthogonal_direction)
34
+ ahead = current.next(orthogonal_direction)
35
+
36
+ if index[behind] || index[ahead]
37
+ total_index = generate_index(@history + [move])
38
+ move_start = find_move_start(current, orthogonal_direction, total_index)
39
+ prefix = Move.new(total_index[move_start], move_start, orthogonal_direction)
40
+ moves << find_move(prefix, total_index)
41
+ end
42
+ end
43
+
44
+ moves
45
+ end
46
+
47
+ def find_move_start(position, direction, index)
48
+ prev = position.previous(direction)
49
+
50
+ if index[prev]
51
+ find_move_start(prev, direction, index)
52
+ else
53
+ position
54
+ end
55
+ end
56
+
57
+ def find_move(prefix, index)
58
+ ahead = prefix.position.shift(prefix.word.length, prefix.direction)
59
+
60
+ if index[ahead]
61
+ find_move(Move.new(prefix.word + index[ahead], prefix.position, prefix.direction), index)
62
+ else
63
+ prefix
64
+ end
65
+ end
66
+
67
+ def generate_index(history)
68
+ index = {}
69
+
70
+ history.each do |move|
71
+ move.word.each_char.with_index do |char, i|
72
+ index[move.position.shift(i, move.direction)] = move.word[i]
73
+ end
74
+ end
75
+
76
+ index
77
+ end
78
+
79
+ def score_move(move, index)
80
+ word_multiplier = 1
81
+ score = move.word.each_char.with_index.inject(0) do |score, (c, i)|
82
+ pos = move.position.shift(i, move.direction)
83
+ if index[pos]
84
+ score + @values[c]
85
+ else
86
+ word_multiplier *= @word_multipliers.fetch(pos, 1)
87
+ score + @values[c] * @letter_multipliers.fetch(pos, 1)
88
+ end
89
+ end * word_multiplier
90
+ score
91
+ end
92
+
93
+ def calculate_tiles_used(move, index)
94
+ used = []
95
+ move.word.each_char.with_index do |char, i|
96
+ next if index[move.position.shift(i, move.direction)]
97
+ used << char
98
+ end
99
+
100
+ used
101
+ end
102
+ end
@@ -0,0 +1,12 @@
1
+ module Direction
2
+ HORIZONTAL = "Horizontal".freeze
3
+ VERTICAL = "Vertical".freeze
4
+
5
+ def self.opposite(direction)
6
+ if direction == HORIZONTAL
7
+ VERTICAL
8
+ else
9
+ HORIZONTAL
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ class Move
2
+ attr_reader :word, :position, :direction
3
+
4
+ def initialize(word, position, direction)
5
+ @word = word
6
+ @position = position
7
+ @direction = direction
8
+ end
9
+
10
+ def ==(other)
11
+ [word, position, direction] == [other.word, other.position, other.direction]
12
+ end
13
+
14
+ def eql?(other)
15
+ [word, position, direction].eql?[other.word, other.position, other.direction]
16
+ end
17
+
18
+ def hash
19
+ [word, position, direction].hash
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ class NotRandom
2
+ def rand(*args)
3
+ 0
4
+ end
5
+ end
@@ -0,0 +1,36 @@
1
+ class Player
2
+ def initialize(board, pouch, rack = [])
3
+ @board = board
4
+ @pouch = pouch
5
+ @score = 0
6
+ @rack = rack
7
+ end
8
+
9
+ def play(move)
10
+ score, tiles_used = @board.play(move)
11
+ @score += score
12
+ remove_tiles(tiles_used)
13
+ end
14
+
15
+ def score
16
+ @score
17
+ end
18
+
19
+ def draw
20
+ need = 7 - @rack.length
21
+ @rack.concat(@pouch.draw(need))
22
+ end
23
+
24
+ def exchange(tiles)
25
+ remove_tiles(tiles)
26
+ @rack.concat(@pouch.exchange(tiles))
27
+ end
28
+
29
+ private
30
+
31
+ def remove_tiles(tiles)
32
+ tiles.each do |tile|
33
+ @rack.slice!(@rack.index(tile))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ require "direction"
2
+
3
+ class Position
4
+ attr_reader :col, :row
5
+
6
+ def initialize(col, row)
7
+ @col = col
8
+ @row = row
9
+ end
10
+
11
+ def shift(offset, direction)
12
+ if direction == Direction::HORIZONTAL
13
+ Position.new(col + offset, row)
14
+ else
15
+ Position.new(col, row + offset)
16
+ end
17
+ end
18
+
19
+ def previous(direction)
20
+ shift(-1, direction)
21
+ end
22
+
23
+ def next(direction)
24
+ shift(1, direction)
25
+ end
26
+
27
+ def ==(other)
28
+ [col, row] == [other.col, other.row]
29
+ end
30
+
31
+ def eql?(other)
32
+ [col, row].eql?([other.col, other.row])
33
+ end
34
+
35
+ def hash
36
+ [col, row].hash
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ class Pouch
2
+ def initialize(tiles, rng = Random.new)
3
+ @tiles = tiles
4
+ @rng = rng
5
+ end
6
+
7
+ def draw(num)
8
+ shuffle
9
+ @tiles.shift(num)
10
+ end
11
+
12
+ def exchange(tiles)
13
+ new_tiles = draw(tiles.length)
14
+ @tiles.concat(tiles)
15
+ new_tiles
16
+ end
17
+
18
+ private
19
+
20
+ def shuffle
21
+ @tiles.sort_by! { @rng.rand }
22
+ end
23
+ end
@@ -0,0 +1,118 @@
1
+ require "board"
2
+ require "move"
3
+ require "position"
4
+
5
+ describe Board do
6
+ it "scores a move" do
7
+ values = { ?C => 2, ?A => 4, ?T => 8 }
8
+ board = Board.new(values)
9
+
10
+ score, _ = board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
11
+
12
+ score.should eq(14)
13
+ end
14
+
15
+ it "scores a multi-word move" do
16
+ values = { ?C => 4, ?A => 1, ?T => 1, ?I => 1, ?N => 2 }
17
+ board = Board.new(values)
18
+
19
+ board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
20
+ score, _ = board.play(Move.new("TIN", Position.new(1, 1), Direction::HORIZONTAL))
21
+
22
+ score.should eq(8)
23
+ end
24
+
25
+ it "returns the tiles used to make the play" do
26
+ values = { ?C => 1, ?A => 4, ?T => 8, ?S => 1 }
27
+ board = Board.new(values)
28
+
29
+ _, tiles_used1 = board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
30
+ _, tiles_used2 = board.play(Move.new("CATS", Position.new(0, 0), Direction::HORIZONTAL))
31
+
32
+ tiles_used1.should eq(%w{C A T})
33
+ tiles_used2.should eq(%w{S})
34
+ end
35
+
36
+ it "scores letter multipliers" do
37
+ values = { ?C => 2, ?A => 4, ?T => 8 }
38
+ letter_multipliers = { Position.new(0, 0) => 2 }
39
+ board = Board.new(values, letter_multipliers)
40
+
41
+ score, _ = board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
42
+
43
+ score.should eq(16)
44
+ end
45
+
46
+ it "scores letter multipliers in every word formed" do
47
+ values = Hash.new(1)
48
+ letter_multipliers = { Position.new(1, 1) => 2 }
49
+ board = Board.new(values, letter_multipliers)
50
+
51
+ board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
52
+ score, _ = board.play(Move.new("TIN", Position.new(1, 1), Direction::HORIZONTAL))
53
+
54
+ score.should eq(9)
55
+ end
56
+
57
+ it "only counts letter multipliers played this turn" do
58
+ values = { ?C => 2, ?A => 4, ?T => 8, ?S => 1 }
59
+ letter_multipliers = { Position.new(0, 0) => 2 }
60
+ board = Board.new(values, letter_multipliers)
61
+
62
+ board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
63
+ score, _ = board.play(Move.new("CATS", Position.new(0, 0), Direction::HORIZONTAL))
64
+
65
+ score.should eq(15)
66
+ end
67
+
68
+ it "scores word multipliers" do
69
+ values = { ?C => 2, ?A => 4, ?T => 8 }
70
+ word_multipliers = { Position.new(0, 0) => 2 }
71
+ board = Board.new(values, {}, word_multipliers)
72
+
73
+ score, _ = board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
74
+
75
+ score.should eq(28)
76
+ end
77
+
78
+ it "scores word multipliers in every word formed" do
79
+ values = Hash.new(1)
80
+ word_multipliers = { Position.new(1, 1) => 2 }
81
+ board = Board.new(values, {}, word_multipliers)
82
+
83
+ board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
84
+ score, _ = board.play(Move.new("TIN", Position.new(1, 1), Direction::HORIZONTAL))
85
+
86
+ score.should eq(12)
87
+ end
88
+
89
+ it "doesn't count word multipliers twice" do
90
+ values = { ?C => 2, ?A => 4, ?T => 8, ?S => 1 }
91
+ word_multipliers = { Position.new(0, 0) => 2 }
92
+ board = Board.new(values, {}, word_multipliers)
93
+
94
+ board.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
95
+ score, _ = board.play(Move.new("CATS", Position.new(0, 0), Direction::HORIZONTAL))
96
+
97
+ score.should eq(15)
98
+ end
99
+
100
+ it "scores a 50 point bingo bonus" do
101
+ values = Hash.new(1)
102
+ board = Board.new(values)
103
+
104
+ score, _ = board.play(Move.new("RAMRODS", Position.new(0, 0), Direction::HORIZONTAL))
105
+
106
+ score.should eq(57)
107
+ end
108
+
109
+ it "only gives bingo bonus if 7 tiles are used" do
110
+ values = Hash.new(1)
111
+ board = Board.new(values)
112
+
113
+ board.play(Move.new("RAM", Position.new(0, 0), Direction::HORIZONTAL))
114
+ score, _ = board.play(Move.new("RAMRODS", Position.new(1, 0), Direction::HORIZONTAL))
115
+
116
+ score.should eq(7)
117
+ end
118
+ end
@@ -0,0 +1,164 @@
1
+ require "player"
2
+ require "board"
3
+ require "move"
4
+ require "position"
5
+ require "pouch"
6
+ require "not_random"
7
+
8
+ describe "Game" do
9
+ def positions_for(*rows)
10
+ positions = {}
11
+ rows.each.with_index do |row, i|
12
+ row.each_char.with_index do |col, j|
13
+ unless col == " "
14
+ positions[Position.new(j, i)] = col.to_i
15
+ end
16
+ end
17
+ end
18
+ positions
19
+ end
20
+
21
+ # View Game: http://www.scrabble-assoc.com/games/nsc2000/1
22
+ # The game has fUZES scored incorrectly, but we get it right.
23
+ it "plays a whole game" do
24
+ tiles = %w{
25
+ G I N O O T T
26
+ A E O Q R S U
27
+ A I M N O T U
28
+ A E F G I T O
29
+ D E E I L P V
30
+ A A B L Y
31
+ A A D D O
32
+ E E R
33
+ A C E H U
34
+ F I P R W
35
+ M O U
36
+ E I J L N
37
+ E S
38
+ D I N O
39
+ H R Z
40
+ A C E K T T W
41
+ f I S
42
+ a N B
43
+ E G N X
44
+ L R V
45
+ R S Y
46
+ E I
47
+ }
48
+
49
+ values = {
50
+ ?A => 1,
51
+ ?B => 3,
52
+ ?C => 3,
53
+ ?D => 2,
54
+ ?E => 1,
55
+ ?F => 4,
56
+ ?G => 2,
57
+ ?H => 4,
58
+ ?I => 1,
59
+ ?J => 8,
60
+ ?K => 5,
61
+ ?L => 1,
62
+ ?M => 3,
63
+ ?N => 1,
64
+ ?O => 1,
65
+ ?P => 3,
66
+ ?Q => 10,
67
+ ?R => 1,
68
+ ?S => 1,
69
+ ?T => 1,
70
+ ?U => 1,
71
+ ?V => 4,
72
+ ?W => 4,
73
+ ?X => 8,
74
+ ?Y => 4,
75
+ ?Z => 10,
76
+ ?a => 0,
77
+ ?f => 0
78
+ }
79
+
80
+ letter_multipliers = positions_for(
81
+ " 2 2 ",
82
+ " 3 3 ",
83
+ " 2 2 ",
84
+ "2 2 2",
85
+ " ",
86
+ " 3 3 3 3 ",
87
+ " 2 2 2 2 ",
88
+ " 2 2 ",
89
+ " 2 2 2 2 ",
90
+ " 3 3 3 3 ",
91
+ " ",
92
+ "2 2 2",
93
+ " 2 2 ",
94
+ " 3 3 ",
95
+ " 2 2 "
96
+ )
97
+
98
+ word_multipliers = positions_for(
99
+ "3 3 3",
100
+ " 2 2 ",
101
+ " 2 2 ",
102
+ " 2 2 ",
103
+ " 2 2 ",
104
+ " ",
105
+ " ",
106
+ "3 2 3",
107
+ " ",
108
+ " ",
109
+ " 2 2 ",
110
+ " 2 2 ",
111
+ " 2 2 ",
112
+ " 2 2 ",
113
+ "3 3 3"
114
+ )
115
+
116
+ shuffle = NotRandom.new
117
+ board = Board.new(values, letter_multipliers, word_multipliers)
118
+ pouch = Pouch.new(tiles, shuffle)
119
+
120
+ p1_rack = []
121
+ p2_rack = []
122
+ p1 = Player.new(board, pouch, p1_rack)
123
+ p2 = Player.new(board, pouch, p2_rack)
124
+
125
+ moves = [
126
+ Move.new("TOOTING", Position.new(3, 7), Direction::HORIZONTAL),
127
+ Move.new("EQUATORS", Position.new(3, 3), Direction::VERTICAL),
128
+ Move.new("MOUNTAIN", Position.new(8, 4), Direction::VERTICAL),
129
+ Move.new("FOGIE", Position.new(7, 0), Direction::VERTICAL),
130
+ Move.new("LIVED", Position.new(7, 10), Direction::VERTICAL),
131
+ Move.new("BAA", Position.new(2, 8), Direction::VERTICAL),
132
+ Move.new("DOPED", Position.new(9, 1), Direction::VERTICAL),
133
+ Move.new("RELAY", Position.new(1, 5), Direction::VERTICAL),
134
+ Move.new("AHA", Position.new(0, 5), Direction::VERTICAL),
135
+ Move.new("PEWIT", Position.new(4, 10), Direction::VERTICAL),
136
+ Move.new("QUA", Position.new(3, 4), Direction::HORIZONTAL),
137
+ Move.new("FLINT", Position.new(0, 14), Direction::HORIZONTAL),
138
+ Move.new("MEOW", Position.new(1, 12), Direction::HORIZONTAL),
139
+ Move.new("REJOINED", Position.new(6, 13), Direction::HORIZONTAL),
140
+ Move.new("CHEZ", Position.new(12, 11), Direction::VERTICAL),
141
+ Move.new("WACKO", Position.new(5, 3), Direction::VERTICAL),
142
+ Move.new("fUZES", Position.new(10, 14), Direction::HORIZONTAL),
143
+ Move.new("BEN", Position.new(9, 9), Direction::VERTICAL),
144
+ Move.new("SEX", Position.new(10, 4), Direction::VERTICAL),
145
+ Move.new("VAR", Position.new(10, 8), Direction::VERTICAL),
146
+ Move.new("GYRO", Position.new(4, 1), Direction::HORIZONTAL),
147
+ Move.new("DEaLT", Position.new(9, 1), Direction::HORIZONTAL),
148
+ Move.new("SIR", Position.new(11, 5), Direction::VERTICAL),
149
+ Move.new("IT", Position.new(13, 0), Direction::HORIZONTAL)
150
+ ]
151
+
152
+ moves.each_slice(2) do |move1, move2|
153
+ p1.draw
154
+ p2.draw
155
+ p1.play(move1)
156
+ p2.play(move2)
157
+ end
158
+
159
+ p1_unplayed = values.values_at(*p1_rack).inject(0, :+)
160
+
161
+ p1.score.should eq(422)
162
+ (p2.score + p1_unplayed * 2).should eq(409)
163
+ end
164
+ end
@@ -0,0 +1,68 @@
1
+ require "player"
2
+ require "move"
3
+ require "position"
4
+
5
+ describe Player do
6
+ let(:board) { mock }
7
+ let(:pouch) { mock }
8
+
9
+ it "draws tiles from the pouch" do
10
+ rack = []
11
+ player = Player.new(board, pouch, rack)
12
+
13
+ pouch.stub(:draw).with(7).and_return(%w{A B C})
14
+ player.draw
15
+
16
+ rack.should eq(%w{A B C})
17
+ end
18
+
19
+ it "only draws the tiles needed" do
20
+ rack = %w{A B C}
21
+ player = Player.new(board, pouch, rack)
22
+
23
+ pouch.stub(:draw).with(4).and_return(%w{D E F})
24
+ player.draw
25
+
26
+ rack.should eq(%w{A B C D E F})
27
+ end
28
+
29
+ it "exchanges tiles with new ones from the pouch" do
30
+ rack = %w{A B C D E F G}
31
+ player = Player.new(board, pouch, rack)
32
+
33
+ pouch.stub(:exchange).with(%w{A B C}).and_return(%w{X Y Z})
34
+ player.exchange(%w{A B C})
35
+
36
+ rack.should eq(%w{D E F G X Y Z})
37
+ end
38
+
39
+ it "uses up tiles to make a move" do
40
+ rack = %w{A B C D E}
41
+ player = Player.new(board, pouch, rack)
42
+
43
+ board.stub(:play).and_return([1, %w{A C E}])
44
+ player.play(Move.new("", Position.new(0, 0), Direction::HORIZONTAL))
45
+
46
+ rack.should eq(%w{B D})
47
+ end
48
+
49
+ it "records her score" do
50
+ player = Player.new(board, pouch)
51
+ move = Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL)
52
+ board.stub(:play).with(move).and_return([1, []])
53
+
54
+ player.play(move)
55
+
56
+ player.score.should eq(1)
57
+ end
58
+
59
+ it "add the scores of multiple plays" do
60
+ player = Player.new(board, pouch)
61
+ board.stub(:play).and_return([1, []])
62
+
63
+ player.play(Move.new("CAT", Position.new(0, 0), Direction::HORIZONTAL))
64
+ player.play(Move.new("COT", Position.new(0, 0), Direction::VERTICAL))
65
+
66
+ player.score.should eq(2)
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ require "pouch"
2
+ require "not_random"
3
+
4
+ describe Pouch do
5
+ it "allows players to draw tiles" do
6
+ pouch = Pouch.new(%w{A B C D E F G}, NotRandom.new)
7
+
8
+ draw1 = pouch.draw(4)
9
+ draw2 = pouch.draw(4)
10
+
11
+ draw1.should eq(%w{A B C D})
12
+ draw2.should eq(%w{E F G})
13
+ end
14
+
15
+ it "allows players to exchange tiles" do
16
+ tiles = %w{A B C D E F G}
17
+ pouch = Pouch.new(tiles, NotRandom.new)
18
+
19
+ pouch.exchange(%w{X Y Z}).should eq(%w{A B C})
20
+ tiles.should eq(%w{D E F G X Y Z})
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nerdword
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tom von Schwerdtner
9
+ - Nestor Walker
10
+ - Eric Ostrich
11
+ - Sam Goldman
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2012-12-14 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: rspec
19
+ requirement: !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ! '>='
23
+ - !ruby/object:Gem::Version
24
+ version: '0'
25
+ type: :development
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ description: Play crossword games with your friends.
34
+ email:
35
+ - dev@smartlogicsolutions.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - lib/board.rb
41
+ - lib/direction.rb
42
+ - lib/move.rb
43
+ - lib/not_random.rb
44
+ - lib/player.rb
45
+ - lib/position.rb
46
+ - lib/pouch.rb
47
+ - spec/board_spec.rb
48
+ - spec/game_spec.rb
49
+ - spec/player_spec.rb
50
+ - spec/pouch_spec.rb
51
+ homepage: http://github.com/smartlogic/nerdword
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.23
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Get nerdy... with words!
75
+ test_files:
76
+ - spec/board_spec.rb
77
+ - spec/game_spec.rb
78
+ - spec/player_spec.rb
79
+ - spec/pouch_spec.rb