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 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: