linotype 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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