xo 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,25 +4,18 @@ require 'xo/grid'
4
4
 
5
5
  module XO
6
6
 
7
- # This class defines an {Evaluator#analyze} method than can be used to look at a grid and
8
- # answer the following questions:
7
+ # {Evaluator} is a {http://en.wikipedia.org/wiki/Singleton_pattern Singleton} that defines an {#analyze} method than can be used to examine a grid to answer the following questions:
9
8
  #
10
- # 1. Is it a valid grid? A grid is considered valid if it possible for
11
- # two players, taking turns, to reach the given grid configuration.
9
+ # 1. Is it a valid grid? A grid is considered valid if it possible for two players, taking turns, to reach the given grid configuration.
12
10
  # 2. Is there a winner/loser or is the grid squashed?
13
11
  # 3. Who is the winner/loser?
14
12
  # 4. Which positions make up the winning row, column and/or diagonal?
15
- #
16
- # The Evaluator class is a Singleton class and can be used as follows:
17
- #
18
- # @example
19
- # Evaluator.instance.analyze(Grid.new('xo'), Grid::X)
20
13
  class Evaluator
21
14
  include Singleton
22
15
 
23
- # Analyze a given grid assuming that the given token is the one that was last placed on it.
16
+ # Examines the given grid assuming that the given token is the one that was last placed on it.
24
17
  #
25
- # It can return a hash in following formats:
18
+ # The following return values are possible:
26
19
  #
27
20
  # - If everything is fine, then
28
21
  #
@@ -30,11 +23,11 @@ module XO
30
23
  #
31
24
  # - If the game is over and the given token is in a winning position, then
32
25
  #
33
- # { status: :game_over, type: :winner, details: [{ where: :a_where, index: :an_index, positions: :the_positions }] }
26
+ # { status: :game_over, type: :winner, details: [{ where: :where, index: :index, positions: :positions }] }
34
27
  #
35
28
  # - If the game is over and the other token is in a winning position, then
36
29
  #
37
- # { status: :game_over, type: :loser, details: [{ where: :a_where, index: :an_index, positions: :the_positions }] }
30
+ # { status: :game_over, type: :loser, details: [{ where: :where, index: :index, positions: :positions }] }
38
31
  #
39
32
  # - If the game is over due to a squashed grid, then
40
33
  #
@@ -48,14 +41,13 @@ module XO
48
41
  #
49
42
  # { status: :invalid_grid, type: :two_winners }
50
43
  #
51
- # Legend:
44
+ # *Legend:*
52
45
  #
53
- # - :a_where is one of :row, :column, :diagonal
54
- # - :an_index is one of 1, 2, 3 if :a_where is :row or :column and one of 1, 2 if :a_where is :diagonal
55
- # - :the_positions is a 3 element array having the row, column values of the winning position
46
+ # - *:where* is one of :row, :column, :diagonal
47
+ # - *:index* is one of 1, 2, 3 if :where is :row or :column and one of 1, 2 if :where is :diagonal
48
+ # - *:positions* is a 3 element array having the row, column values of the winning position
56
49
  #
57
- # Notice that the :details key is an array since it is possible to win a game in two different ways. For
58
- # example:
50
+ # Notice that the :details key is an Array since it is possible to win a game in two different ways. For example:
59
51
  #
60
52
  # x | o | x
61
53
  # ---+---+---
@@ -63,15 +55,13 @@ module XO
63
55
  # ---+---+---
64
56
  # x | o | x
65
57
  #
66
- # # Position (2, 2) would have to be the last position played for this to happen.
67
- #
68
- # @param grid [Grid] the grid to be analyzed
58
+ # @param grid [Grid] the grid to be examined
69
59
  # @param token [Grid::X, Grid::O] the token that was last placed on the grid
70
60
  # @raise [ArgumentError] unless token is either {Grid::X} or {Grid::O}
71
61
  # @return [Hash]
72
62
  def analyze(grid, token)
73
63
  check_token(token)
74
- init_analyzer(grid, token)
64
+ initialize_analyzer(grid, token)
75
65
  perform_analysis
76
66
  end
77
67
 
@@ -79,17 +69,17 @@ module XO
79
69
  #
80
70
  # @example
81
71
  # g = Grid.new('xoxxo')
82
- # xs, os = Evaluator.instance.xos(g)
72
+ # xs, os = Evaluator.xos(g)
83
73
  # puts xs # => 3
84
74
  # puts os # => 2
85
75
  #
86
76
  # @return [Array(Integer, Integer)]
87
- def xos(grid)
77
+ def self.xos(grid)
88
78
  xs = os = 0
89
79
 
90
- grid.each do |_, _, val|
91
- xs += 1 if val == Grid::X
92
- os += 1 if val == Grid::O
80
+ grid.each do |_, _, k|
81
+ xs += 1 if k == Grid::X
82
+ os += 1 if k == Grid::O
93
83
  end
94
84
 
95
85
  [xs, os]
@@ -103,30 +93,22 @@ module XO
103
93
  raise ArgumentError, "illegal token #{token}" unless Grid.is_token?(token)
104
94
  end
105
95
 
106
- def init_analyzer(grid, token)
107
- @grid = grid
108
- @token = token
96
+ def initialize_analyzer(grid, token)
97
+ @grid = grid
98
+ @token = token
109
99
  @winners = {}
110
100
  end
111
101
 
112
102
  def perform_analysis
113
- return { status: :invalid_grid, type: :too_many_moves_ahead } if two_or_more_moves_ahead?
103
+ return { status: :invalid_grid, type: :too_many_moves_ahead } if two_or_more_moves_ahead?
114
104
 
115
105
  find_winners
116
106
 
117
- if two_winners?
118
- { status: :invalid_grid, type: :two_winners }
119
- elsif winners[token]
120
- { status: :game_over, type: :winner, details: winners[token] }
121
- elsif winners[other_token]
122
- { status: :game_over, type: :loser, details: winners[other_token] }
123
- else
124
- if grid.full?
125
- { status: :game_over, type: :squashed }
126
- else
127
- { status: :ok }
128
- end
129
- end
107
+ return { status: :invalid_grid, type: :two_winners } if two_winners?
108
+ return { status: :game_over, type: :winner, details: winners[token] } if winners[token]
109
+ return { status: :game_over, type: :loser, details: winners[other_token] } if winners[other_token]
110
+ return { status: :game_over, type: :squashed } if grid.full?
111
+ return { status: :ok }
130
112
  end
131
113
 
132
114
  def two_or_more_moves_ahead?
@@ -134,34 +116,32 @@ module XO
134
116
  end
135
117
 
136
118
  def moves_ahead
137
- xs, os = xos(grid)
119
+ xs, os = self.class.xos(grid)
138
120
 
139
121
  (xs - os).abs
140
122
  end
141
123
 
142
- def find_winners
143
- winning_positions.each do |w|
144
- a = grid[*w[:positions][0]]
145
- b = grid[*w[:positions][1]]
146
- c = grid[*w[:positions][2]]
124
+ WINNING_POSITIONS = [
125
+ { where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] },
126
+ { where: :row, index: 2, positions: [[2, 1], [2, 2], [2, 3]] },
127
+ { where: :row, index: 3, positions: [[3, 1], [3, 2], [3, 3]] },
147
128
 
148
- add_winner(a, w) if Grid.is_token?(a) && a == b && b == c
149
- end
150
- end
129
+ { where: :column, index: 1, positions: [[1, 1], [2, 1], [3, 1]] },
130
+ { where: :column, index: 2, positions: [[1, 2], [2, 2], [3, 2]] },
131
+ { where: :column, index: 3, positions: [[1, 3], [2, 3], [3, 3]] },
151
132
 
152
- def winning_positions
153
- @winning_positions ||= [
154
- { where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] },
155
- { where: :row, index: 2, positions: [[2, 1], [2, 2], [2, 3]] },
156
- { where: :row, index: 3, positions: [[3, 1], [3, 2], [3, 3]] },
133
+ { where: :diagonal, index: 1, positions: [[1, 1], [2, 2], [3, 3]] },
134
+ { where: :diagonal, index: 2, positions: [[1, 3], [2, 2], [3, 1]] }
135
+ ]
157
136
 
158
- { where: :column, index: 1, positions: [[1, 1], [2, 1], [3, 1]] },
159
- { where: :column, index: 2, positions: [[1, 2], [2, 2], [3, 2]] },
160
- { where: :column, index: 3, positions: [[1, 3], [2, 3], [3, 3]] },
137
+ def find_winners
138
+ WINNING_POSITIONS.each do |w|
139
+ x = grid[*w[:positions][0]]
140
+ y = grid[*w[:positions][1]]
141
+ z = grid[*w[:positions][2]]
161
142
 
162
- { where: :diagonal, index: 1, positions: [[1, 1], [2, 2], [3, 3]] },
163
- { where: :diagonal, index: 2, positions: [[1, 3], [2, 2], [3, 1]] }
164
- ]
143
+ add_winner(x, w.dup) if winning_combination(x, y, z)
144
+ end
165
145
  end
166
146
 
167
147
  def add_winner(token, details)
@@ -172,8 +152,12 @@ module XO
172
152
  end
173
153
  end
174
154
 
155
+ def winning_combination(x, y, z)
156
+ Grid.is_token?(x) && x == y && y == z
157
+ end
158
+
175
159
  def two_winners?
176
- winners[Grid::X] && winners[Grid::O]
160
+ winners.keys.length == 2
177
161
  end
178
162
 
179
163
  def other_token
@@ -12,103 +12,54 @@ module XO
12
12
  # 2 | |
13
13
  # ---+---+---
14
14
  # 3 | |
15
- #
16
- # It is important to note that if a position stores anything other than
17
- # {X} or {O} then that position is considered to be open.
18
15
  class Grid
19
16
 
20
17
  X = :x
21
18
  O = :o
22
19
 
20
+ EMPTY = :e
21
+
23
22
  ROWS = 3
24
23
  COLS = 3
25
24
 
26
25
  N = ROWS * COLS
27
26
 
28
- # Determines whether or not position (r, c) is such that 1 <= r <= 3 and 1 <= c <= 3.
29
- #
30
- # @param r [Integer] the row
31
- # @param c [Integer] the column
32
- # @return [Boolean] true iff the position is contained within a 3x3 grid
33
27
  def self.contains?(r, c)
34
28
  r.between?(1, ROWS) && c.between?(1, COLS)
35
29
  end
36
30
 
37
- # Classifies what is and isn't considered to be a token.
38
- #
39
- # @param val [Object]
40
- # @return [Boolean] true iff val is {X} or {O}
41
- def self.is_token?(val)
42
- val == X || val == O
31
+ def self.is_token?(k)
32
+ k == X || k == O
43
33
  end
44
34
 
45
- # Determines the other token.
46
- #
47
- # @param val [Object]
48
- # @return [Object] {X} given {O}, {O} given {X} or the original value
49
- def self.other_token(val)
50
- val == X ? O : (val == O ? X : val)
35
+ def self.other_token(k)
36
+ k == X ? O : (k == O ? X : k)
51
37
  end
52
38
 
53
- attr_reader :grid
54
- private :grid
55
-
56
- # Creates a new empty grid by default. You can also create a
57
- # prepopulated grid by passing in a string representation.
58
- #
59
- # @example
60
- # g = Grid.new('xo ox o')
61
39
  def initialize(g = '')
62
40
  @grid = from_string(g)
63
41
  end
64
42
 
65
- # Creates a copy of the given grid. Use #dup to get your copy.
66
- #
67
- # @example
68
- # g = Grid.new
69
- # g_copy = g.dup
70
- #
71
- # @param orig [Grid] the original grid
72
- # @return [Grid] a copy
73
43
  def initialize_copy(orig)
74
44
  @grid = orig.instance_variable_get(:@grid).dup
75
45
  end
76
46
 
77
- # Determines whether or not there are any tokens on the grid.
78
- #
79
- # @return [Boolean] true iff there are no tokens on the grid
80
47
  def empty?
81
- grid.all? { |val| !self.class.is_token?(val) }
48
+ grid.all? { |k| !self.class.is_token?(k) }
82
49
  end
83
50
 
84
- # Determines whether or not every position on the grid has a token?
85
- #
86
- # @return [Boolean] true iff every position on the grid has a token
87
51
  def full?
88
- grid.all? { |val| self.class.is_token?(val) }
52
+ grid.all? { |k| self.class.is_token?(k) }
89
53
  end
90
54
 
91
- # Sets position (r, c) to the given value.
92
- #
93
- # @param r [Integer] the row
94
- # @param c [Integer] the column
95
- # @param val [Object]
96
- # @raise [IndexError] if the position is off the grid
97
- # @return [Object] the value it was given
98
- def []=(r, c, val)
55
+ def []=(r, c, k)
99
56
  if self.class.contains?(r, c)
100
- grid[idx(r, c)] = self.class.is_token?(val) ? val : :e
57
+ grid[idx(r, c)] = normalize(k)
101
58
  else
102
59
  raise IndexError, "position (#{r}, #{c}) is off the grid"
103
60
  end
104
61
  end
105
62
 
106
- # Retrieves the value at the given position (r, c).
107
- #
108
- # @param r [Integer] the row
109
- # @param c [Integer] the column
110
- # @raise [IndexError] if the position is off the grid
111
- # @return [Object]
112
63
  def [](r, c)
113
64
  if self.class.contains?(r, c)
114
65
  grid[idx(r, c)]
@@ -117,29 +68,20 @@ module XO
117
68
  end
118
69
  end
119
70
 
120
- # Determines whether or not position (r, c) contains a token.
121
- #
122
- # @param r [Integer] the row
123
- # @param c [Integer] the column
124
- # @raise [IndexError] if the position is off the grid
125
- # @return true iff the position does not contain a token
126
71
  def open?(r, c)
127
72
  !self.class.is_token?(self[r, c])
128
73
  end
129
74
 
130
- # Removes all tokens from the grid.
131
75
  def clear
132
- grid.fill(:e)
133
-
134
- self
76
+ grid.fill(EMPTY)
135
77
  end
136
78
 
137
- # Used for iterating over all the positions of the grid from left to right and top to bottom.
79
+ # Iterates over all the positions of this grid from left to right and top to bottom.
138
80
  #
139
81
  # @example
140
82
  # g = Grid.new
141
- # g.each do |r, c, val|
142
- # puts "(#{r}, #{c}) -> #{val}"
83
+ # g.each do |r, c, k|
84
+ # puts "(#{r}, #{c}) -> #{k}"
143
85
  # end
144
86
  def each
145
87
  (1..ROWS).each do |r|
@@ -149,7 +91,7 @@ module XO
149
91
  end
150
92
  end
151
93
 
152
- # Used for iterating over all the open positions of the grid from left to right and top to bottom.
94
+ # Iterates over all the open positions of this grid from left to right and top to bottom.
153
95
  #
154
96
  # @example
155
97
  # g = Grid.new
@@ -164,16 +106,14 @@ module XO
164
106
  self.each { |r, c, _| yield(r, c) if open?(r, c) }
165
107
  end
166
108
 
167
- # Returns a string representation of the grid which may be useful
168
- # for debugging.
109
+ # Returns a string representation of this grid which can be useful for debugging.
169
110
  def inspect
170
- grid.map { |val| t(val) }.join
111
+ grid.map { |k| t(k) }.join
171
112
  end
172
113
 
173
- # Returns a string representation of the grid which may be useful
174
- # for display.
114
+ # Returns a string representation of this grid which can be useful for display.
175
115
  def to_s
176
- g = grid.map { |val| t(val) }
116
+ g = grid.map { |k| t(k) }
177
117
 
178
118
  [" #{g[0]} | #{g[1]} | #{g[2]} ",
179
119
  "---+---+---",
@@ -184,21 +124,23 @@ module XO
184
124
 
185
125
  private
186
126
 
187
- def from_string(g)
188
- g = g.to_s
189
- l = g.length
127
+ attr_reader :grid
190
128
 
191
- g = if l < N
192
- g + ' ' * (N - l)
193
- elsif l > N
194
- g[0..N-1]
195
- else
196
- g
129
+ def from_string(s)
130
+ adjust_length(s, N).split('').map do |ch|
131
+ normalize(ch.to_sym)
197
132
  end
133
+ end
134
+
135
+ def adjust_length(s, n)
136
+ l = s.length
198
137
 
199
- g.split('').map do |ch|
200
- sym = ch.to_sym
201
- sym == X || sym == O ? sym : :e
138
+ if l < n
139
+ s + ' ' * (n - l)
140
+ elsif l > n
141
+ s[0..n-1]
142
+ else
143
+ s
202
144
  end
203
145
  end
204
146
 
@@ -217,8 +159,14 @@ module XO
217
159
  COLS * (r - 1) + (c - 1)
218
160
  end
219
161
 
220
- def t(val)
221
- self.class.is_token?(val) ? val : ' '
162
+ NORMALIZED_TO_STRING_MAP = { X => 'x', O => 'o', EMPTY => ' ' }
163
+
164
+ def t(k)
165
+ NORMALIZED_TO_STRING_MAP[normalize(k)]
166
+ end
167
+
168
+ def normalize(k)
169
+ self.class.is_token?(k) ? k : EMPTY
222
170
  end
223
171
  end
224
172
  end
@@ -1,3 +1,3 @@
1
1
  module XO
2
- VERSION = '1.0.0'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -0,0 +1,323 @@
1
+ require 'spec_helper'
2
+
3
+ class EngineObserver
4
+
5
+ def initialize
6
+ @events = []
7
+ end
8
+
9
+ def last_event
10
+ @events.last
11
+ end
12
+
13
+ def update(event)
14
+ @events << event
15
+ end
16
+ end
17
+
18
+ module XO
19
+
20
+ describe Engine do
21
+
22
+ let (:engine) { Engine.new }
23
+ let (:engine_observer) { EngineObserver.new }
24
+
25
+ describe "how the engine works" do
26
+
27
+ before { engine.add_observer(engine_observer) }
28
+
29
+ describe "in the Init state" do
30
+
31
+ it "is in the Init state" do
32
+ engine.current_state.must_equal Init
33
+ end
34
+
35
+ describe "#start" do
36
+
37
+ describe "for valid input" do
38
+
39
+ before { engine.start(Grid::X) }
40
+
41
+ it "is X's turn" do
42
+ engine.turn.must_equal Grid::X
43
+ end
44
+
45
+ it "has an empty grid" do
46
+ engine.grid.must_be :empty?
47
+ end
48
+
49
+ it "transitions to the Playing state" do
50
+ engine.current_state.must_equal Playing
51
+ end
52
+
53
+ it "triggers an event" do
54
+ engine_observer.last_event[:name].must_equal :game_started
55
+ end
56
+ end
57
+
58
+ describe "for invalid input" do
59
+
60
+ it "raises ArgumentError" do
61
+ proc { engine.start(:nobody) }.must_raise ArgumentError
62
+ end
63
+ end
64
+ end
65
+
66
+ it "raises StateDesignPattern::IllegalStateException for the actions stop, play and continue_playing" do
67
+ proc { engine.stop }.must_raise StateDesignPattern::IllegalStateException
68
+ proc { engine.play(1, 1) }.must_raise StateDesignPattern::IllegalStateException
69
+ proc { engine.continue_playing(Grid::X) }.must_raise StateDesignPattern::IllegalStateException
70
+ end
71
+ end
72
+
73
+ describe "in the Playing state" do
74
+
75
+ before { engine.start(Grid::O) }
76
+
77
+ it "is in the Playing state" do
78
+ engine.current_state.must_equal Playing
79
+ end
80
+
81
+ describe "#play" do
82
+
83
+ describe "when the move is out of bounds" do
84
+
85
+ before { engine.play(1, 0) }
86
+
87
+ it "remains in the Playing state" do
88
+ engine.current_state.must_equal Playing
89
+ end
90
+
91
+ it "triggers an event" do
92
+ last_event = engine_observer.last_event
93
+
94
+ last_event[:name].must_equal :invalid_move
95
+ last_event[:type].must_equal :out_of_bounds
96
+ end
97
+ end
98
+
99
+ describe "when the move is on an occupied position" do
100
+
101
+ before { engine.play(1, 1).play(1, 1) }
102
+
103
+ it "remains in the Playing state" do
104
+ engine.current_state.must_equal Playing
105
+ end
106
+
107
+ it "triggers an event" do
108
+ last_event = engine_observer.last_event
109
+
110
+ last_event[:name].must_equal :invalid_move
111
+ last_event[:type].must_equal :occupied
112
+ end
113
+ end
114
+
115
+ describe "when the move results in a win" do
116
+
117
+ before do
118
+ engine.play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
119
+ end
120
+
121
+ it "transitions to the GameOver state" do
122
+ engine.current_state.must_equal GameOver
123
+ end
124
+
125
+ it "triggers an event" do
126
+ last_event = engine_observer.last_event
127
+
128
+ last_event[:name].must_equal :game_over
129
+ last_event[:type].must_equal :winner
130
+ last_event[:last_move].must_equal({ turn: Grid::O, r: 1, c: 3 })
131
+ last_event[:details][0][:where].must_equal :row
132
+ last_event[:details][0][:index].must_equal 1
133
+ end
134
+ end
135
+
136
+ describe "when the move results in a squashed game" do
137
+
138
+ before do
139
+ engine
140
+ .play(1, 1).play(2, 2)
141
+ .play(3, 3).play(1, 2)
142
+ .play(3, 2).play(3, 1)
143
+ .play(1, 3).play(2, 3)
144
+ .play(2, 1)
145
+ end
146
+
147
+ it "transitions to the GameOver state" do
148
+ engine.current_state.must_equal GameOver
149
+ end
150
+
151
+ it "triggers an event" do
152
+ last_event = engine_observer.last_event
153
+
154
+ last_event[:name].must_equal :game_over
155
+ last_event[:type].must_equal :squashed
156
+ last_event[:last_move].must_equal({ turn: Grid::O, r: 2, c: 1 })
157
+ end
158
+ end
159
+
160
+ describe "when the move just advances the game" do
161
+
162
+ before { engine.play(1, 1) }
163
+
164
+ it "sets O at position (1, 1)" do
165
+ engine.grid[1, 1].must_equal Grid::O
166
+ end
167
+
168
+ it "remains in the Playing state" do
169
+ engine.current_state.must_equal Playing
170
+ end
171
+
172
+ it "triggers an event" do
173
+ last_event = engine_observer.last_event
174
+
175
+ last_event[:name].must_equal :next_turn
176
+ last_event[:last_move].must_equal({ turn: Grid::O, r: 1, c: 1 })
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "#stop" do
182
+
183
+ before { engine.play(2, 2).play(3, 1).stop }
184
+
185
+ it "resets the game" do
186
+ engine.turn.must_equal :nobody
187
+ engine.grid.must_be :empty?
188
+ end
189
+
190
+ it "transitions to the Init state" do
191
+ engine.current_state.must_equal Init
192
+ end
193
+
194
+ it "triggers an event" do
195
+ engine_observer.last_event[:name].must_equal :game_stopped
196
+ end
197
+ end
198
+
199
+ it "raises StateDesignPattern::IllegalStateException for the actions start and continue_playing" do
200
+ proc { engine.start(Grid::X) }.must_raise StateDesignPattern::IllegalStateException
201
+ proc { engine.continue_playing(Grid::X) }.must_raise StateDesignPattern::IllegalStateException
202
+ end
203
+ end
204
+
205
+ describe "in the GameOver state" do
206
+
207
+ describe "due to a win" do
208
+
209
+ before do
210
+ engine
211
+ .start(Grid::X)
212
+ .play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
213
+ end
214
+
215
+ it "is in the GameOver state" do
216
+ engine.current_state.must_equal GameOver
217
+ end
218
+
219
+ describe "#continue_playing" do
220
+
221
+ before { engine.continue_playing }
222
+
223
+ it "is X's turn" do
224
+ engine.turn.must_equal Grid::X
225
+ end
226
+
227
+ it "has an empty grid" do
228
+ engine.grid.must_be :empty?
229
+ end
230
+
231
+ it "transitions to the Playing state" do
232
+ engine.current_state.must_equal Playing
233
+ end
234
+
235
+ it "triggers an event" do
236
+ last_event = engine_observer.last_event
237
+
238
+ last_event[:name].must_equal :game_started
239
+ last_event[:type].must_equal :continue_playing
240
+ end
241
+ end
242
+ end
243
+
244
+ describe "due to a squashed game" do
245
+
246
+ before do
247
+ engine
248
+ .start(Grid::X)
249
+ .play(1, 1).play(2, 2)
250
+ .play(3, 3).play(1, 2)
251
+ .play(3, 2).play(3, 1)
252
+ .play(1, 3).play(2, 3)
253
+ .play(2, 1)
254
+ end
255
+
256
+ it "is in the GameOver state" do
257
+ engine.current_state.must_equal GameOver
258
+ end
259
+
260
+ describe "#continue_playing" do
261
+
262
+ before { engine.continue_playing }
263
+
264
+ it "is O's turn" do
265
+ engine.turn.must_equal Grid::O
266
+ end
267
+
268
+ it "has an empty grid" do
269
+ engine.grid.must_be :empty?
270
+ end
271
+
272
+ it "transitions to the Playing state" do
273
+ engine.current_state.must_equal Playing
274
+ end
275
+
276
+ it "triggers an event" do
277
+ last_event = engine_observer.last_event
278
+
279
+ last_event[:name].must_equal :game_started
280
+ last_event[:type].must_equal :continue_playing
281
+ end
282
+ end
283
+ end
284
+
285
+ describe "#stop" do
286
+
287
+ before do
288
+ engine
289
+ .start(Grid::X)
290
+ .play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
291
+ .stop
292
+ end
293
+
294
+ it "resets the game" do
295
+ engine.turn.must_equal :nobody
296
+ engine.grid.must_be :empty?
297
+ end
298
+
299
+ it "transitions to the Init state" do
300
+ engine.current_state.must_equal Init
301
+ end
302
+
303
+ it "triggers an event" do
304
+ engine_observer.last_event[:name].must_equal :game_stopped
305
+ end
306
+ end
307
+
308
+ describe "what happens when calling illegal actions" do
309
+ before do
310
+ engine
311
+ .start(Grid::X)
312
+ .play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
313
+ end
314
+
315
+ it "raises StateDesignPattern::IllegalStateException for the actions start and play" do
316
+ proc { engine.start(Grid::X) }.must_raise StateDesignPattern::IllegalStateException
317
+ proc { engine.play(1, 1) }.must_raise StateDesignPattern::IllegalStateException
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end