Class: Upwords::Player

Inherits:
Object
  • Object
show all
Defined in:
lib/upwords/player.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, rack_capacity = 7, cpu = false) ⇒ Player

Returns a new instance of Player.



10
11
12
13
14
15
16
# File 'lib/upwords/player.rb', line 10

def initialize(name, rack_capacity=7, cpu=false)
  @name = name
  @rack = LetterRack.new(rack_capacity)
  @score = 0
  @last_turn = nil
  @cpu = cpu
end

Instance Attribute Details

#last_turnObject

Returns the value of attribute last_turn.



8
9
10
# File 'lib/upwords/player.rb', line 8

def last_turn
  @last_turn
end

#nameObject (readonly)

Returns the value of attribute name.



7
8
9
# File 'lib/upwords/player.rb', line 7

def name
  @name
end

#scoreObject

Returns the value of attribute score.



8
9
10
# File 'lib/upwords/player.rb', line 8

def score
  @score
end

Instance Method Details

#cpu?Boolean


AI move methods


Returns:

  • (Boolean)


84
85
86
# File 'lib/upwords/player.rb', line 84

def cpu?
  @cpu
end

#cpu_move(board, dict, batch_size = 1000, min_score = 0) ⇒ Object

Execute a legal move based on a predefined strategy

Basic strategy:

  • Find all legal move shapes and all possible letter permutations across those shapes (this computation is relatively quick)

  • Retun the highest score from permutation that do not produce in any illegal new words (this computation is slow…)

  • To speed up the above computation: + Only check a batch of permutations at a time (specified in ‘batch_size’ argument) + After each batch, terminate the subroutine if it finds a score that is at least as high as the given ‘min_score’ + Decrement the ‘min_score’ after each batch that does not terminate the subroutine to prevent endless searches

TODO: refactor the the ‘strategy’ component out of this method, so different strategies can be swapped in and out



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/upwords/player.rb', line 135

def cpu_move(board, dict, batch_size = 1000, min_score = 0)
  possible_moves = self.legal_move_shapes_letter_permutations(board)      
  possible_moves.shuffle!
  
  top_score = 0
  top_score_move = nil
  
  while top_score_move.nil? || (top_score < min_score) do
    
    # Check if next batch contains any legal moves and save the top score
    ([batch_size, possible_moves.size].min).times do
      move_arr = possible_moves.pop
      move = Move.new(move_arr)

      if move.legal_words?(board, dict)
        move_score = move.score(board, self)
        if move_score >= top_score
          top_score = move_score
          top_score_move = move_arr
        end
      end
    end
    
    # Decrement minimum required score after each cycle to help prevent long searches
    min_score = [(min_score - 1), 0].max
  end
  
  top_score_move
end

Return a list of legal move shapes that player could make on board



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/upwords/player.rb', line 89

def legal_move_shapes(board)
  one_space_moves = board.coordinates.map {|posn| [posn]}
  
  # Collect board positions grouped by rows
  (0...board.num_rows).map do |row| 
    (0...board.num_columns).map {|col| [row, col]} 
    
    # Collect all positions of all possible horizontal multi-position moves that player could make
  end.flat_map do |posns|
    (2..(letters.size)).flat_map {|sz| posns.combination(sz).to_a}
    
    # Collect all positions of all possible vertical and horizontal moves that player could make
  end.reduce(one_space_moves) do |all_moves, move|
    all_moves << move                           # Horizontal moves
    all_moves << move.map {|posn| posn.rotate}  # Vertical moves
    
    # Filter out illegal move shapes  
  end.select do |move_posns|
    Shape.new(move_posns).legal?(board)
  end
end

Return list of all possible letter permutations on legal move shapes that player could make on board Elements in the list will be in form of [(row, col), letter]



113
114
115
116
117
118
119
120
121
122
# File 'lib/upwords/player.rb', line 113

def legal_move_shapes_letter_permutations(board)
  # Cache result of letter permutation computation for each move size
  letter_perms = Hash.new {|perms, sz| perms[sz] = letters.permutation(sz).to_a}
  
  legal_move_shapes(board).reduce([]) do |all_moves, move|
    letter_perms[move.size].reduce(all_moves) do |move_perms, perm|
      move_perms << move.zip(perm)
    end
  end
end

#lettersObject



18
19
20
# File 'lib/upwords/player.rb', line 18

def letters
  @rack.letters.dup
end

#play_letter(board, letter, row, col) ⇒ Object



54
55
56
57
58
59
60
61
62
# File 'lib/upwords/player.rb', line 54

def play_letter(board, letter, row, col)
  rack_letter = @rack.remove(letter)
  begin
    board.play_letter(rack_letter, row, col)
  rescue IllegalMove => exn
    take_letter(rack_letter)
    raise IllegalMove, exn
  end
end

#rack_capacityObject



34
35
36
# File 'lib/upwords/player.rb', line 34

def rack_capacity
  @rack.capacity
end

#rack_empty?Boolean

Returns:

  • (Boolean)


30
31
32
# File 'lib/upwords/player.rb', line 30

def rack_empty?
  @rack.empty?
end

#rack_full?Boolean

Returns:

  • (Boolean)


26
27
28
# File 'lib/upwords/player.rb', line 26

def rack_full?
  @rack.full?
end

#refill_rack(letter_bank) ⇒ Object



74
75
76
77
78
# File 'lib/upwords/player.rb', line 74

def refill_rack(letter_bank)
  until rack_full? || letter_bank.empty? do
    take_letter(letter_bank.draw)
  end
end

#show_rack(masked = false) ⇒ Object



22
23
24
# File 'lib/upwords/player.rb', line 22

def show_rack(masked = false)
  masked ? @rack.show_masked : @rack.show
end

#swap_letter(letter, letter_bank) ⇒ Object



64
65
66
67
68
69
70
71
72
# File 'lib/upwords/player.rb', line 64

def swap_letter(letter, letter_bank)
  if letter_bank.empty?
    raise IllegalMove, "Letter bank is empty!"
  else
    trade_letter = @rack.remove(letter)
    take_letter(letter_bank.draw)
    letter_bank.deposit(trade_letter)
  end
end

#take_from(board, row, col) ⇒ Object


Game object interaction methods




46
47
48
49
50
51
52
# File 'lib/upwords/player.rb', line 46

def take_from(board, row, col)
  if board.stack_height(row, col) == 0
    raise IllegalMove, "No letters in #{row}, #{col}!"
  else
    take_letter(board.remove_top_letter(row, col))
  end
end

#take_letter(letter) ⇒ Object



38
39
40
# File 'lib/upwords/player.rb', line 38

def take_letter(letter)
  @rack.add(letter)
end