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 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
- [ {:letter=>"I", :row=>0, :column=>0, :covered_by=>nil, :defended=>false},
24
- {:letter=>"E", :row=>0, :column=>1, :covered_by=>nil, :defended=>false},
25
- {:letter=>"X", :row=>0, :column=>2, :covered_by=>nil, :defended=>false},
26
- {:letter=>"A", :row=>0, :column=>3, :covered_by=>nil, :defended=>false},
27
- {:letter=>"C", :row=>0, :column=>4, :covered_by=>nil, :defended=>false}
28
- ],
29
- [ {:letter=>"R", :row=>1, :column=>0, :covered_by=>nil, :defended=>false},
30
- {:letter=>"F", :row=>1, :column=>1, :covered_by=>nil, :defended=>false},
31
- {:letter=>"D", :row=>1, :column=>2, :covered_by=>nil, :defended=>false},
32
- {:letter=>"S", :row=>1, :column=>3, :covered_by=>nil, :defended=>false},
33
- {:letter=>"B", :row=>1, :column=>4, :covered_by=>nil, :defended=>false}
34
- ],
35
- [ {:letter=>"B", :row=>2, :column=>0, :covered_by=>nil, :defended=>false},
36
- {:letter=>"L", :row=>2, :column=>1, :covered_by=>nil, :defended=>false},
37
- {:letter=>"K", :row=>2, :column=>2, :covered_by=>nil, :defended=>false},
38
- {:letter=>"Q", :row=>2, :column=>3, :covered_by=>nil, :defended=>false},
39
- {:letter=>"P", :row=>2, :column=>4, :covered_by=>nil, :defended=>false}
40
- ],
41
- [ {:letter=>"S", :row=>3, :column=>0, :covered_by=>nil, :defended=>false},
42
- {:letter=>"O", :row=>3, :column=>1, :covered_by=>nil, :defended=>false},
43
- {:letter=>"K", :row=>3, :column=>2, :covered_by=>nil, :defended=>false},
44
- {:letter=>"P", :row=>3, :column=>3, :covered_by=>nil, :defended=>false},
45
- {:letter=>"W", :row=>3, :column=>4, :covered_by=>nil, :defended=>false}
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.play({ row: 3, column: 1 }, {row: 3, column: 0})
52
+ > next_play = game.best_next_play
53
+
54
+ > game.play(*next_play[:coordinates])
60
55
  => true
61
56
 
62
- > game.moves
63
- => [{:player=>1, :word=>"OS", :valid=>true, :invalid_reason=>nil, :player_sequence=>1, :total_sequence=>1}
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
@@ -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, rows=5, columns=5)
13
- new(game, tiles: rows.times.collect { columns.times.collect { ('A'..'Z').to_a[rand(0..25)] } })
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
- def initialize
5
- @board = Board.new_random(self)
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
- move = Move.new(self, @current_player, tiles)
14
- if move.valid?
15
- move.cover_tiles!
16
- toggle_current_player
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
- @moves << move
19
- @current_player == move.player ? false : true
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 ||= dictionary.words.select { |word| (word.chars.to_a - letters).empty? }
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[:row]][tile_coordinate[:column]]
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.each { |tile| tile.covered_by = @player unless tile.defended? }
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
@@ -19,6 +19,10 @@ module Linotype
19
19
  }
20
20
  end
21
21
 
22
+ def to_a
23
+ [row, column]
24
+ end
25
+
22
26
  def game
23
27
  @board.game
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Linotype
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linotype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: