xo 1.0.0 → 1.1.0

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.
@@ -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