Class: Kalah

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

Defined Under Namespace

Classes: IllegalMoveError, IllegalStateError

Constant Summary collapse

NUM_HOUSES =

Number of houses.

6
STORE_INDEX_P1 =

Index of players’ stores.

NUM_HOUSES
STORE_INDEX_P2 =
NUM_HOUSES * 2 + 1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(num_seeds = 6) ⇒ Kalah

Returns a new instance of Kalah.



20
21
22
23
24
25
26
27
# File 'lib/kalah.rb', line 20

def initialize(num_seeds = 6)
  @stores = Array.new(NUM_HOUSES * 2 + 2, num_seeds)
  @stores[STORE_INDEX_P1] = 0
  @stores[STORE_INDEX_P2] = 0
  @history = Array.new
  @current_player = :P1
  @num_seeds = num_seeds
end

Instance Attribute Details

#current_playerObject (readonly)

Returns the value of attribute current_player.



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

def current_player
  @current_player
end

#historyObject (readonly)

Returns the value of attribute history.



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

def history
  @history
end

#num_seedsObject (readonly)

Returns the value of attribute num_seeds.



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

def num_seeds
  @num_seeds
end

#storesObject (readonly)

Returns the value of attribute stores.



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

def stores
  @stores
end

Instance Method Details

#current_players_housesObject

Returns the index-range of the current player’s houses.



133
134
135
136
137
138
139
# File 'lib/kalah.rb', line 133

def current_players_houses
  if @current_player == :P1
    player_1_houses
  else
    player_2_houses
  end
end

#current_players_storeObject

Returns the index of the current player’s store.



117
118
119
# File 'lib/kalah.rb', line 117

def current_players_store
  return @current_player == :P1 ? STORE_INDEX_P1 : STORE_INDEX_P2
end

#has_current_player_wonObject

Checks whether current player’s houses are empty.



122
123
124
125
126
127
128
129
130
# File 'lib/kalah.rb', line 122

def has_current_player_won
  current_players_houses.each do |i|
    if @stores[i] != 0
      return false
    end
  end

  true
end

#player_1_housesObject

The range of player 1’s houses



142
143
144
# File 'lib/kalah.rb', line 142

def player_1_houses
  0..(STORE_INDEX_P1 - 1)
end

#player_2_housesObject

The range of player 2’s houses



147
148
149
# File 'lib/kalah.rb', line 147

def player_2_houses
  (STORE_INDEX_P1 + 1)..(STORE_INDEX_P2 - 1)
end

#sow(index) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/kalah.rb', line 29

def sow(index)
  if index < 0 or index > @stores.length
    raise IllegalMoveError, "Index #{i} does not exist (Number of houses is #{@stores.length})"
  elsif index == STORE_INDEX_P1 or index == STORE_INDEX_P2
    raise IllegalMoveError, "Player must not sow seeds from the store"
  elsif has_current_player_won
    raise IllegalMoveError, "Game is over"
  elsif (current_player == :P1 and index > STORE_INDEX_P1) or
        (current_player == :P2 and index < STORE_INDEX_P1)
    raise IllegalMoveError, "Current player must not sow from the opponent's houses"
  elsif @stores[index] == 0
    raise IllegalMoveError, "House is empty."
  end
  
  # Copy to history.
  @history.push({ player: current_player, stores: Array.new(@stores) })

  # Perform sow
  num_seeds = @stores[index]
  # Empty house.
  @stores[index] = 0
  # Sow in other houses.
  i = 1
  while num_seeds > 0 do
    house = (index + i) % @stores.length
    if (@current_player == :P1 and house == STORE_INDEX_P2) or
       (@current_player == :P2 and house == STORE_INDEX_P1)
      # Skip opponent's store.
      i += 1
      next
    end

    @stores[house] += 1
    num_seeds -= 1
    i += 1
  end

  # If last seed lands in empty house (size now 1), take opposite store.
  if current_players_houses.cover?((index + i - 1) % @stores.length) and
      i > NUM_HOUSES and
      @stores[(index + i - 1) % @stores.length] == 1
    opposite = NUM_HOUSES * 2 - ((index + i - 1) % @stores.length)
    # Put to store.
    @stores[current_players_store] += @stores[opposite]
    @stores[current_players_store] += 1
    # Empty houses.
    @stores[opposite] = 0
    @stores[(index + i - 1) % @stores.length] = 0
  end

  # Check if game has ended.
  if has_current_player_won
    # Move opponent's seeds to current player's store.
    range, store = 0
    if @current_player == :P1
      range = player_2_houses
      store = STORE_INDEX_P1
    else
      range = player_1_houses
      store = STORE_INDEX_P2
    end

    range.each do |i|
      @stores[store] += @stores[i]
      @stores[i] = 0
    end
  end

  # Change turn if last seed did not land in own store.
  if !has_current_player_won and (index + i - 1) % @stores.length != current_players_store
    @current_player = @current_player == :P1 ? :P2 : :P1
  end
end

#to_sObject

Prints the Kalah board



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/kalah.rb', line 152

def to_s
  # Combine p2 (top) and p1 (bot) houses, and stores (mid)
  top = "   "
  mid = ""
  bot = "   "
  @stores.each_with_index do |v, i|
    if i == STORE_INDEX_P1
      mid << "%02d\n" % v
    elsif i == STORE_INDEX_P2
      mid.insert(0, "\n%02d " % v)
    elsif i > STORE_INDEX_P1
      top.insert(3, "%02d " % v)
    else
      bot << "%02d " % v
      mid << "   "
    end
  end

  top << mid << bot

end

#undo(moves = 1) ⇒ Object

Undoes a number of moves.

Raises:



104
105
106
107
108
109
110
111
112
113
# File 'lib/kalah.rb', line 104

def undo(moves = 1)
  raise IllegalStateError, "Cannot undo #{moves} moves. "\
                           "Only #{@history.length} moves performed."\
                           if history.length < moves
  
  state = @history.pop
  @current_player = state[:player]
  @stores = state[:stores]
  
end