nerdword 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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