linotype 0.0.2 → 0.0.3
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.
- data/README.md +44 -43
- data/lib/linotype/board.rb +6 -2
- data/lib/linotype/game.rb +101 -19
- data/lib/linotype/move.rb +33 -4
- data/lib/linotype/tile.rb +4 -0
- data/lib/linotype/version.rb +1 -1
- metadata +1 -1
data/README.md
CHANGED
@@ -19,57 +19,60 @@ Here's a basic rundown of the primary public methods:
|
|
19
19
|
> game = Linotype::Game.new
|
20
20
|
|
21
21
|
> game.board
|
22
|
-
=> [
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
]
|
47
|
-
[ {:letter=>"L", :row=>4, :column=>0, :covered_by=>nil, :defended=>false},
|
48
|
-
{:letter=>"U", :row=>4, :column=>1, :covered_by=>nil, :defended=>false},
|
49
|
-
{:letter=>"J", :row=>4, :column=>2, :covered_by=>nil, :defended=>false},
|
50
|
-
{:letter=>"Y", :row=>4, :column=>3, :covered_by=>nil, :defended=>false},
|
51
|
-
{:letter=>"D", :row=>4, :column=>4, :covered_by=>nil, :defended=>false}
|
52
|
-
]
|
53
|
-
]
|
22
|
+
=> [[{:letter=>"N", :row=>0, :column=>0, :covered_by=>1, :defended=>true},
|
23
|
+
{:letter=>"S", :row=>0, :column=>1, :covered_by=>1, :defended=>false},
|
24
|
+
{:letter=>"L", :row=>0, :column=>2, :covered_by=>nil, :defended=>false},
|
25
|
+
{:letter=>"K", :row=>0, :column=>3, :covered_by=>nil, :defended=>false},
|
26
|
+
{:letter=>"W", :row=>0, :column=>4, :covered_by=>nil, :defended=>false}],
|
27
|
+
[{:letter=>"T", :row=>1, :column=>0, :covered_by=>1, :defended=>true},
|
28
|
+
{:letter=>"P", :row=>1, :column=>1, :covered_by=>1, :defended=>false},
|
29
|
+
{:letter=>"Y", :row=>1, :column=>2, :covered_by=>nil, :defended=>false},
|
30
|
+
{:letter=>"E", :row=>1, :column=>3, :covered_by=>1, :defended=>false},
|
31
|
+
{:letter=>"E", :row=>1, :column=>4, :covered_by=>nil, :defended=>false}],
|
32
|
+
[{:letter=>"L", :row=>2, :column=>0, :covered_by=>1, :defended=>false},
|
33
|
+
{:letter=>"O", :row=>2, :column=>1, :covered_by=>nil, :defended=>false},
|
34
|
+
{:letter=>"L", :row=>2, :column=>2, :covered_by=>nil, :defended=>false},
|
35
|
+
{:letter=>"Z", :row=>2, :column=>3, :covered_by=>nil, :defended=>false},
|
36
|
+
{:letter=>"Z", :row=>2, :column=>4, :covered_by=>nil, :defended=>false}],
|
37
|
+
[{:letter=>"T", :row=>3, :column=>0, :covered_by=>nil, :defended=>false},
|
38
|
+
{:letter=>"B", :row=>3, :column=>1, :covered_by=>nil, :defended=>false},
|
39
|
+
{:letter=>"W", :row=>3, :column=>2, :covered_by=>nil, :defended=>false},
|
40
|
+
{:letter=>"M", :row=>3, :column=>3, :covered_by=>nil, :defended=>false},
|
41
|
+
{:letter=>"F", :row=>3, :column=>4, :covered_by=>nil, :defended=>false}],
|
42
|
+
[{:letter=>"B", :row=>4, :column=>0, :covered_by=>nil, :defended=>false},
|
43
|
+
{:letter=>"O", :row=>4, :column=>1, :covered_by=>nil, :defended=>false},
|
44
|
+
{:letter=>"F", :row=>4, :column=>2, :covered_by=>nil, :defended=>false},
|
45
|
+
{:letter=>"M", :row=>4, :column=>3, :covered_by=>nil, :defended=>false},
|
46
|
+
{:letter=>"V", :row=>4, :column=>4, :covered_by=>nil, :defended=>false}]]
|
54
47
|
|
55
48
|
> # Takes about 8 seconds on a laptop -- brute force solution
|
56
49
|
> game.potential_plays
|
57
50
|
=> ["WORD","MATCHES","FROM","DICTIONARY","IN","AN","ARRAY"]
|
58
51
|
|
59
|
-
> game.
|
52
|
+
> next_play = game.best_next_play
|
53
|
+
|
54
|
+
> game.play(*next_play[:coordinates])
|
60
55
|
=> true
|
61
56
|
|
62
|
-
> game.moves
|
63
|
-
=>
|
57
|
+
> game.moves.last.to_hash
|
58
|
+
=> { :player=>1,
|
59
|
+
:word=>"SPLENT",
|
60
|
+
:valid=>true,
|
61
|
+
:coordinates=>[[0, 1], [1, 1], [2, 0], [1, 3], [0, 0], [1, 0]],
|
62
|
+
:invalid_reason=>nil,
|
63
|
+
:player_sequence=>1,
|
64
|
+
:total_sequence=>1,
|
65
|
+
:score => {
|
66
|
+
:defended_before=>0,
|
67
|
+
:covered_before=>0,
|
68
|
+
:defended_after=>2,
|
69
|
+
:covered_after=>6,
|
70
|
+
:defended=>2,
|
71
|
+
:covered=>6}
|
72
|
+
}
|
64
73
|
|
65
74
|
> game.scores
|
66
75
|
=> {1=>2, 2=>0}
|
67
|
-
|
68
|
-
> game.play({ row: 1, column: 0 }, {row: 1, column: 1})
|
69
|
-
=> false
|
70
|
-
|
71
|
-
> game.moves.last
|
72
|
-
=> {:player=>2, :word=>"RF", :valid=>false, :invalid_reason=>"is not in dictionary", :player_sequence=>1, :total_sequence=>1}
|
73
76
|
|
74
77
|
# pass by playing a nil move
|
75
78
|
> game.play
|
@@ -89,10 +92,8 @@ The default dictionary is based on the the [Internet Scrabble Club's TWL06 dicti
|
|
89
92
|
|
90
93
|
## Feature Ideas
|
91
94
|
|
92
|
-
* Game board loading from string
|
93
95
|
* Game board loading from iOS screen shot
|
94
96
|
* Word suggestions (faster)
|
95
|
-
* Game simulation with strategy suggestion
|
96
97
|
* One player command line game
|
97
98
|
* Vowel/consonant ratio setting for random boards
|
98
99
|
* Built in dictionaries for different languages or topics
|
data/lib/linotype/board.rb
CHANGED
@@ -9,8 +9,12 @@ module Linotype
|
|
9
9
|
@tiles = args[:tiles].collect { |row| row.collect { |tile| Tile.new(self, tile) } }
|
10
10
|
end
|
11
11
|
|
12
|
-
def self.new_random(game
|
13
|
-
new(game,
|
12
|
+
def self.new_random(game)
|
13
|
+
new(game, new_random_letters)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.new_random_letters(rows=5, columns=5)
|
17
|
+
rows.times.collect { columns.times.collect { ('A'..'Z').to_a[rand(0..25)] } }
|
14
18
|
end
|
15
19
|
|
16
20
|
def row_count
|
data/lib/linotype/game.rb
CHANGED
@@ -1,22 +1,89 @@
|
|
1
1
|
module Linotype
|
2
2
|
class Game
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
attr_accessor :moves
|
5
|
+
attr_reader :current_player
|
6
|
+
|
7
|
+
def initialize(args={})
|
8
|
+
@board = Board.new(self, tiles: create_tile_letter_array(args[:tiles]))
|
6
9
|
@players = [Player.new, Player.new]
|
7
10
|
@current_player = @players[0]
|
8
11
|
@moves = []
|
9
12
|
end
|
13
|
+
|
14
|
+
def create_tile_letter_array(letter_arg)
|
15
|
+
case letter_arg
|
16
|
+
when nil
|
17
|
+
Board.new_random_letters
|
18
|
+
when Array
|
19
|
+
letter_arg
|
20
|
+
when String
|
21
|
+
square_size = Math.sqrt(letter_arg.length).ceil
|
22
|
+
letter_array = [[]]
|
23
|
+
letter_arg.each_char do |letter|
|
24
|
+
letter_array << [] if letter_array.last.size == square_size
|
25
|
+
letter_array.last << letter.upcase
|
26
|
+
end
|
27
|
+
letter_array
|
28
|
+
end
|
29
|
+
end
|
10
30
|
|
11
31
|
def play(*tile_coordinates)
|
12
32
|
tiles = find_tiles(tile_coordinates)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
33
|
+
Move.new(self, @current_player, tiles).valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
def every_play_for_word(word)
|
37
|
+
tiles_per_letter = {}
|
38
|
+
word.chars.to_a.uniq.each do |unique_letter|
|
39
|
+
tiles_per_letter[unique_letter] = []
|
40
|
+
tile_rows.flatten.select { |tile| tile.letter == unique_letter }.each do |matching_tile|
|
41
|
+
tiles_per_letter[unique_letter] << matching_tile
|
42
|
+
end
|
43
|
+
end
|
44
|
+
variations = tiles_per_letter.values.inject(1) { |vars, tiles| tiles.count * vars }
|
45
|
+
plays = []
|
46
|
+
v = 0
|
47
|
+
variations.times { plays << []; v += 1 }
|
48
|
+
word.chars.each do |letter|
|
49
|
+
play_number = 0
|
50
|
+
repetitions = variations / tiles_per_letter[letter].count
|
51
|
+
tiles_per_letter[letter].each do |tile|
|
52
|
+
repetitions.times do
|
53
|
+
plays[play_number] << tile
|
54
|
+
play_number += 1
|
55
|
+
end
|
56
|
+
end
|
17
57
|
end
|
18
|
-
|
19
|
-
|
58
|
+
plays.select { |play| play.uniq.count == play.count }
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_potential_plays
|
62
|
+
potential_moves = []
|
63
|
+
potential_plays.each do |word_to_test|
|
64
|
+
every_play_for_word(word_to_test).each do |tiles|
|
65
|
+
move = Move.new(self, @current_player, tiles)
|
66
|
+
potential_moves << move
|
67
|
+
undo_last_move!
|
68
|
+
end
|
69
|
+
end
|
70
|
+
potential_moves.collect(&:to_hash)
|
71
|
+
end
|
72
|
+
|
73
|
+
def valid_potential_plays
|
74
|
+
test_potential_plays.select { |potential_play| potential_play[:valid] }
|
75
|
+
end
|
76
|
+
|
77
|
+
def best_next_play
|
78
|
+
valid_potential_plays.sort { |a, b| b[:score][:defended] <=> a[:score][:defended] }.first
|
79
|
+
end
|
80
|
+
|
81
|
+
def undo_last_move!
|
82
|
+
if (last_move = moves.pop) && last_move.valid?
|
83
|
+
last_move.uncover_tiles!
|
84
|
+
@current_player = other_player if last_move.valid?
|
85
|
+
end
|
86
|
+
moves.last.cover_tiles! if moves.any?
|
20
87
|
end
|
21
88
|
|
22
89
|
def over?
|
@@ -35,10 +102,6 @@ module Linotype
|
|
35
102
|
tile_rows.collect { |row| row.collect { |tile| tile.to_hash } }
|
36
103
|
end
|
37
104
|
|
38
|
-
def moves
|
39
|
-
@moves.collect { |move| move.to_hash }
|
40
|
-
end
|
41
|
-
|
42
105
|
def player_number(player)
|
43
106
|
@players.index(player) + 1
|
44
107
|
end
|
@@ -47,8 +110,27 @@ module Linotype
|
|
47
110
|
covered_tiles(player).count
|
48
111
|
end
|
49
112
|
|
50
|
-
def potential_plays
|
51
|
-
@potential_plays ||=
|
113
|
+
def potential_plays(remaining_letters=letters)
|
114
|
+
@potential_plays ||= calc_potential_plays(remaining_letters)
|
115
|
+
end
|
116
|
+
|
117
|
+
def calc_potential_plays(remaining_letters)
|
118
|
+
plays = []
|
119
|
+
letter_group = letters.group_by { |l| l }
|
120
|
+
dictionary.words.each do |word|
|
121
|
+
if word_match(letter_group, word)
|
122
|
+
plays << word
|
123
|
+
end
|
124
|
+
end
|
125
|
+
plays
|
126
|
+
end
|
127
|
+
|
128
|
+
def word_match(letter_group, word)
|
129
|
+
word_letter_group = word.chars.to_a.group_by { |c| c }
|
130
|
+
word.each_char do |letter|
|
131
|
+
return false unless (letter_group[letter] && letter_group[letter].count >= word_letter_group[letter].count)
|
132
|
+
end
|
133
|
+
true
|
52
134
|
end
|
53
135
|
|
54
136
|
def valid_moves
|
@@ -74,13 +156,11 @@ module Linotype
|
|
74
156
|
def other_player
|
75
157
|
@players.index(@current_player) == 0 ? @players[1] : @players[0]
|
76
158
|
end
|
77
|
-
private :other_player
|
78
159
|
|
79
160
|
def find_tiles(tile_coordinates)
|
80
|
-
puts tile_coordinates.inspect
|
81
161
|
return [] if tile_coordinates.empty?
|
82
162
|
tile_coordinates.collect do |tile_coordinate|
|
83
|
-
tile = tile_rows[tile_coordinate[
|
163
|
+
tile = tile_rows[tile_coordinate[0]][tile_coordinate[1]]
|
84
164
|
raise ArgumentError, "The board does not have a tile at that location" unless tile
|
85
165
|
tile
|
86
166
|
end
|
@@ -90,17 +170,19 @@ module Linotype
|
|
90
170
|
def toggle_current_player
|
91
171
|
@current_player = other_player
|
92
172
|
end
|
93
|
-
private :toggle_current_player
|
94
173
|
|
95
174
|
def uncovered_tiles
|
96
175
|
covered_tiles(nil)
|
97
176
|
end
|
98
177
|
private :uncovered_tiles
|
99
178
|
|
179
|
+
def defended_tiles(player)
|
180
|
+
tile_rows.flatten.select { |tile| tile.covered_by == player && tile.defended? }
|
181
|
+
end
|
182
|
+
|
100
183
|
def covered_tiles(player)
|
101
184
|
tile_rows.flatten.select { |tile| tile.covered_by == player }
|
102
185
|
end
|
103
|
-
private :covered_tiles
|
104
186
|
|
105
187
|
def two_passes_in_a_row?
|
106
188
|
valid_moves.count >= 2 && valid_moves[-2,2].select { |move| move.pass? }.count == 2
|
data/lib/linotype/move.rb
CHANGED
@@ -1,15 +1,37 @@
|
|
1
1
|
module Linotype
|
2
2
|
class Move
|
3
3
|
|
4
|
-
attr_reader :game, :player, :invalid_reason
|
4
|
+
attr_reader :game, :player, :invalid_reason, :score
|
5
5
|
|
6
6
|
def initialize(game, player, tiles)
|
7
7
|
@game = game
|
8
8
|
@player = player
|
9
9
|
@tiles = tiles
|
10
10
|
calculate_valid
|
11
|
+
if valid?
|
12
|
+
calculate_scores(:before)
|
13
|
+
cover_tiles!
|
14
|
+
calculate_scores(:after)
|
15
|
+
calculate_net_scores
|
16
|
+
@game.toggle_current_player
|
17
|
+
end
|
18
|
+
@game.moves << self
|
19
|
+
end
|
20
|
+
|
21
|
+
def score
|
22
|
+
@score ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def calculate_scores(time)
|
26
|
+
score["defended_#{time}".to_sym] = @game.defended_tiles(player).count
|
27
|
+
score["covered_#{time}".to_sym] = @game.covered_tiles(player).count
|
11
28
|
end
|
12
|
-
|
29
|
+
|
30
|
+
def calculate_net_scores
|
31
|
+
score[:defended] = score[:defended_after] - score[:defended_before]
|
32
|
+
score[:covered] = score[:covered_after] - score[:covered_before]
|
33
|
+
end
|
34
|
+
|
13
35
|
def valid?
|
14
36
|
!!@valid
|
15
37
|
end
|
@@ -27,7 +49,12 @@ module Linotype
|
|
27
49
|
end
|
28
50
|
|
29
51
|
def cover_tiles!
|
30
|
-
@tiles.
|
52
|
+
@previously_defended_tiles = @tiles.select { |tile| tile.defended? }
|
53
|
+
(@tiles - @previously_defended_tiles).each { |tile| tile.covered_by = @player }
|
54
|
+
end
|
55
|
+
|
56
|
+
def uncover_tiles!
|
57
|
+
(@tiles - @previously_defended_tiles).each { |tile| tile.covered_by = nil }
|
31
58
|
end
|
32
59
|
|
33
60
|
def to_hash
|
@@ -35,9 +62,11 @@ module Linotype
|
|
35
62
|
player: game.player_number(@player),
|
36
63
|
word: word,
|
37
64
|
valid: valid?,
|
65
|
+
coordinates: @tiles.collect(&:to_a),
|
38
66
|
invalid_reason: @invalid_reason,
|
39
67
|
player_sequence: game.valid_moves.select { |move| move.player == @player }.index(self).to_i + 1,
|
40
|
-
total_sequence: game.valid_moves.index(self).to_i + 1
|
68
|
+
total_sequence: game.valid_moves.index(self).to_i + 1,
|
69
|
+
score: score
|
41
70
|
}
|
42
71
|
end
|
43
72
|
|
data/lib/linotype/tile.rb
CHANGED
data/lib/linotype/version.rb
CHANGED