xo 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module XO
2
+
3
+ module AI
4
+
5
+ class Player
6
+
7
+ attr_reader :token
8
+
9
+ def initialize(token)
10
+ @token = token
11
+ end
12
+
13
+ def terminal_score(outcome)
14
+ send("#{outcome}_value")
15
+ end
16
+
17
+ def non_terminal_score(next_grids, scores)
18
+ best_score(compute_next_grids_scores(next_grids, scores))
19
+ end
20
+
21
+ def best_score(next_grids_scores)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def winner_value
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def loser_value
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def squashed_value
34
+ 0
35
+ end
36
+
37
+ private
38
+
39
+ def compute_next_grids_scores(next_grids, scores)
40
+ next_grids.map { |grid| scores[grid] }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,256 +1,54 @@
1
+ require 'state_design_pattern'
2
+
1
3
  require 'xo/grid'
2
4
  require 'xo/evaluator'
5
+ require 'xo/engine/game_context'
6
+ require 'xo/engine/init'
3
7
 
4
8
  module XO
5
9
 
6
- # A state machine that encapsulates the game logic for Tic-tac-toe. The operation
7
- # of the engine is completely determined by the properties:
8
- #
9
- # - {#state},
10
- # - {#turn},
11
- # - {#grid}, and
12
- # - {#last_event}.
10
+ # The {Engine} encapsulates the game logic for Tic-tac-toe.
13
11
  #
14
- # The engine can be in one of the 3 following states (represented by a symbol):
12
+ # It is controlled by 4 actions:
15
13
  #
16
- # - :init
17
- # - :playing
18
- # - :game_over
14
+ # - start
15
+ # - stop
16
+ # - play
17
+ # - continue_playing
19
18
  #
20
- # The engine begins in the :init state. And, the following methods are used to advance
21
- # a single game of Tic-tac-toe (and transition the engine between its states) by obeying
22
- # the standard rules of Tic-tac-toe:
19
+ # Each action may or may not change the state of the engine. This is important
20
+ # because it is the state of the engine that determines when the above actions
21
+ # can be called.
23
22
  #
24
- # - {#start}: [:init]
25
- # - {#stop}: [:playing, :game_over]
26
- # - {#play}: [:playing]
27
- # - {#continue_playing}: [:game_over]
23
+ # The engine can be in 1 of 3 states:
28
24
  #
29
- # The array of symbols after each method lists the states in which the method is allowed
30
- # to be called.
25
+ # - {Init}
26
+ # - {Playing}
27
+ # - {GameOver}
31
28
  #
32
- # @example
33
- # e = Engine.new
34
- # e.start(Grid::X).play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
29
+ # The engine begins life in the {Init} state.
35
30
  #
36
- # event = e.last_event
37
- # puts event[:name] # => :game_over
38
- # puts event[:type] # => :winner
39
- # puts event[:last_move][:turn] # => Grid::X
31
+ # Here's a table showing which actions can be called in a given state:
40
32
  #
41
- # e.continue_playing(Grid::O).play(2, 2).play(1, 1).play(3, 3).play(1, 3).play(1, 2).play(3, 2).play(2, 1).play(2, 3).play(3, 1)
33
+ # State | Actions
34
+ # ----------------------------------
35
+ # Init | start
36
+ # Playing | play, stop
37
+ # GameOver | continue_playing, stop
42
38
  #
43
- # event = e.last_event
44
- # puts event[:name] # => :game_over
45
- # puts event[:type] # => :squashed
46
- # puts event[:last_move][:turn] # => Grid::O
47
- class Engine
48
-
49
- # @return [:init, :playing, :game_over]
50
- attr_reader :state
51
-
52
- # @return [Grid::X, Grid::O, :nobody]
53
- attr_reader :turn
54
-
55
- # @return [Hash]
56
- attr_reader :last_event
57
-
58
- # Creates a new {Engine} with its state set to :init, turn set to :nobody, an empty grid and
59
- # last_event set to { name: :new }.
60
- def initialize
61
- @grid = Grid.new
62
-
63
- reset
64
-
65
- set_event(:new)
66
- end
39
+ # Each action is defined in their respective state class.
40
+ class Engine < StateDesignPattern::StateMachine
67
41
 
68
- # Get the grid that's managed by the engine.
69
- #
70
- # @return [Grid] a copy of the grid that the engine uses
71
- def grid
72
- @grid.dup
42
+ def start_state
43
+ Init
73
44
  end
74
45
 
75
- # If the current turn is either {Grid::X}, {Grid::O} or :nobody then
76
- # it returns {Grid::O}, {Grid::X}, :nobody respectively.
77
- #
78
- # @return [Grid::X, Grid::O, :nobody]
79
- def next_turn
80
- Grid.other_token(turn)
46
+ def initial_context
47
+ GameContext.new(:nobody, Grid.new)
81
48
  end
82
49
 
83
- # Transitions the engine from the :init state into the :playing state.
84
- #
85
- # Sets the last event to be:
86
- #
87
- # { name: :game_started }
88
- #
89
- # @param turn [Grid::X, Grid::O] the token to have first play
90
- # @raise [ArgumentError] unless turn is either {Grid::X} or {Grid::O}
91
- # @raise [IllegalStateError] unless it's called in the :init state
92
- # @return [self]
93
- def start(turn)
94
- check_turn(turn)
95
-
96
- case state
97
- when :init
98
- handle_start(turn)
99
- else
100
- raise IllegalStateError, "must be in the :init state but state = :#{state}"
101
- end
102
- end
103
-
104
- # Transitions the engine from the :playing or :game_over state into the :game_over state.
105
- #
106
- # Sets the last event to be:
107
- #
108
- # { name: :game_over }
109
- #
110
- # @raise [IllegalStateError] unless it's called in the :playing or :game_over state
111
- # @return [self]
112
- def stop
113
- case state
114
- when :playing, :game_over
115
- handle_stop
116
- else
117
- raise IllegalStateError, "must be in the :playing or :game_over state but state = :#{state}"
118
- end
119
- end
120
-
121
- # Makes a move at the given position (r, c) which may transition the engine into the :game_over state
122
- # or leave it in the :playing state.
123
- #
124
- # Sets the last event as follows:
125
- #
126
- # - If the position is out of bounds, then
127
- #
128
- # { name: :invalid_move, type: :out_of_bounds }
129
- #
130
- # - If the position is occupied, then
131
- #
132
- # { name: :invalid_move, type: :occupied }
133
- #
134
- # - If the move was allowed and didn't result in ending the game, then
135
- #
136
- # { name: :next_turn, last_move: { turn: :a_token, r: :a_row, c: :a_column } }
137
- #
138
- # - If the move was allowed and resulted in a win, then
139
- #
140
- # { name: :game_over, type: :winner, last_move: { turn: :a_token, r: :a_row, c: :a_column }, details: :the_details }
141
- #
142
- # - If the move was allowed and resulted in a squashed game, then
143
- #
144
- # { name: :game_over, type: :squashed, last_move: { turn: :a_token, r: :a_row, c: :a_column } }
145
- #
146
- # Legend:
147
- #
148
- # - :a_token is one of {Grid::X} or {Grid::O}
149
- # - :a_row is one of 1, 2 or 3
150
- # - :a_column is one of 1, 2 or 3
151
- # - :the_details is taken verbatim from the :details key of the returned hash of {Evaluator.analyze}
152
- #
153
- # @param r [Integer] the row
154
- # @param c [Integer] the column
155
- # @raise [IllegalStateError] unless it's called in the :playing state
156
- # @return [self]
157
- def play(r, c)
158
- case state
159
- when :playing
160
- handle_play(r, c)
161
- else
162
- raise IllegalStateError, "must be in the :playing state but state = :#{state}"
163
- end
164
- end
165
-
166
- # Similar to start but should only be used to play another round when a game has ended. It transitions
167
- # the engine from the :game_over state into the :playing state.
168
- #
169
- # Sets the last event to be:
170
- #
171
- # { name: :game_started, type: :continue_playing }
172
- #
173
- # @param turn [Grid::X, Grid::O] the token to have first play
174
- # @raise [ArgumentError] unless turn is either {Grid::X} or {Grid::O}
175
- # @raise [IllegalStateError] unless it's called in the :game_over state
176
- # @return [self]
177
- def continue_playing(turn)
178
- check_turn(turn)
179
-
180
- case state
181
- when :game_over
182
- handle_continue_playing(turn)
183
- else
184
- raise IllegalStateError, "must be in the :game_over state but state = :#{state}"
185
- end
50
+ def evaluator
51
+ Evaluator.instance
186
52
  end
187
-
188
- # The exception raised by {#start}, {#stop}, {#play} and {#continue_playing} whenever
189
- # these methods are called and the engine is in the wrong state.
190
- class IllegalStateError < StandardError; end
191
-
192
- private
193
-
194
- def handle_start(turn)
195
- @state = :playing
196
- @turn = turn
197
- @grid.clear
198
-
199
- set_event(:game_started)
200
- end
201
-
202
- def handle_stop
203
- reset
204
-
205
- set_event(:game_stopped)
206
- end
207
-
208
- def handle_play(r, c)
209
- return set_event(:invalid_move, type: :out_of_bounds) unless Grid.contains?(r, c)
210
- return set_event(:invalid_move, type: :occupied) unless @grid.open?(r, c)
211
-
212
- @grid[r, c] = turn
213
- last_move = { turn: turn, r: r, c: c }
214
-
215
- result = Evaluator.instance.analyze(@grid, turn)
216
-
217
- case result[:status]
218
- when :ok
219
- @turn = next_turn
220
- set_event(:next_turn, last_move: last_move)
221
- when :game_over
222
- @state = :game_over
223
-
224
- case result[:type]
225
- when :winner
226
- set_event(:game_over, type: :winner, last_move: last_move, details: result[:details])
227
- when :squashed
228
- set_event(:game_over, type: :squashed, last_move: last_move)
229
- end
230
- end
231
- end
232
-
233
- def handle_continue_playing(turn)
234
- @turn = turn
235
- @state = :playing
236
- @grid.clear
237
-
238
- set_event(:game_started, type: :continue_playing)
239
- end
240
-
241
- def check_turn(turn)
242
- raise ArgumentError, "illegal token #{turn}" unless Grid.is_token?(turn)
243
- end
244
-
245
- def reset
246
- @state = :init
247
- @turn = :nobody
248
- @grid.clear
249
- end
250
-
251
- def set_event(name, message = {})
252
- @last_event = { name: name }.merge(message)
253
- self
254
- end
255
53
  end
256
54
  end
@@ -0,0 +1,28 @@
1
+ require 'xo/grid'
2
+
3
+ module XO
4
+
5
+ class GameContext < Struct.new(:turn, :grid)
6
+
7
+ def reset
8
+ set_turn_and_clear_grid(:nobody)
9
+ end
10
+
11
+ def set_turn_and_clear_grid(turn)
12
+ self.turn = turn
13
+ grid.clear
14
+ end
15
+
16
+ def switch_turns
17
+ self.turn = next_turn
18
+ end
19
+
20
+ def next_turn
21
+ Grid.other_token(turn)
22
+ end
23
+
24
+ def check_turn(turn)
25
+ raise ArgumentError, "invalid turn symbol, #{turn}" unless Grid.is_token?(turn)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'xo/engine/game_state'
2
+
3
+ module XO
4
+
5
+ class GameOver < GameState
6
+
7
+ # Stops and resets a game.
8
+ #
9
+ # The engine is transitioned into the {Init} state and the event
10
+ #
11
+ # { name: :game_stopped }
12
+ #
13
+ # is triggered.
14
+ def stop
15
+ stop_game
16
+ end
17
+
18
+ # Starts another new game.
19
+ #
20
+ # The token that won plays first, otherwise the game was squashed and so
21
+ # the next token plays first.
22
+ #
23
+ # The engine is transitioned into the {Playing} state and the event
24
+ #
25
+ # { name: :game_started, type: :continue_playing }
26
+ #
27
+ # is triggered.
28
+ def continue_playing
29
+ game_context.set_turn_and_clear_grid(game_context.turn)
30
+ engine.transition_to_state_and_send_event(Playing, :game_started, type: :continue_playing)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ require 'state_design_pattern'
2
+
3
+ module XO
4
+
5
+ class GameState < StateDesignPattern::BaseState
6
+ def_actions :start, :stop, :play, :continue_playing
7
+
8
+ private
9
+
10
+ alias_method :engine, :state_machine
11
+
12
+ def game_context
13
+ engine.context
14
+ end
15
+
16
+ def stop_game
17
+ game_context.reset
18
+ engine.transition_to_state_and_send_event(Init, :game_stopped)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ require 'xo/engine/game_state'
2
+ require 'xo/engine/playing'
3
+
4
+ module XO
5
+
6
+ class Init < GameState
7
+
8
+ # Starts a new game.
9
+ #
10
+ # The engine is transitioned into the {Playing} state and the event
11
+ #
12
+ # { name: :game_started }
13
+ #
14
+ # is triggered.
15
+ #
16
+ # @param turn [Grid::X, Grid::O] specifies which token has first play
17
+ # @raise [ArgumentError] unless turn is either {Grid::X} or {Grid::O}
18
+ def start(turn)
19
+ game_context.check_turn(turn)
20
+ game_context.set_turn_and_clear_grid(turn)
21
+ engine.transition_to_state_and_send_event(Playing, :game_started)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,88 @@
1
+ require 'xo/engine/game_state'
2
+ require 'xo/engine/game_over'
3
+
4
+ module XO
5
+
6
+ class Playing < GameState
7
+
8
+ # Stops and resets a game.
9
+ #
10
+ # The engine is transitioned into the {Init} state and the event
11
+ #
12
+ # { name: :game_stopped }
13
+ #
14
+ # is triggered.
15
+ def stop
16
+ stop_game
17
+ end
18
+
19
+ # Attempts to make a move at the given position (r, c).
20
+ #
21
+ # The following outcomes are possible:
22
+ #
23
+ # - If the position is *out* *of* *bounds*, then the event below is
24
+ # triggered and the engine remains in this state.
25
+ #
26
+ # { name: :invalid_move, type: :out_of_bounds }
27
+ #
28
+ # - If the position is *occupied*, then the event below is triggered and the
29
+ # engine remains in this state.
30
+ #
31
+ # { name: :invalid_move, type: :occupied }
32
+ #
33
+ # - If the move results in a *win*, then the event below is triggered and
34
+ # the engine is transitioned into the {GameOver} state.
35
+ #
36
+ # { name: :game_over, type: :winner, last_move: { turn: :token, r: :row, c: :column }, details: :details }
37
+ #
38
+ # - If the move results in a *squashed* game, then the event below is
39
+ # triggered and the engine is transitioned into the {GameOver} state.
40
+ #
41
+ # { name: :game_over, type: :squashed, last_move: { turn: :next_token, r: :row, c: :column } }
42
+ #
43
+ # - Otherwise, the event below is triggered and the engine remains in this
44
+ # state.
45
+ #
46
+ # { name: :next_turn, last_move: { turn: :token, r: :row, c: :column } }
47
+ #
48
+ # *Legend:*
49
+ #
50
+ # - *:token* is one of {Grid::X} or {Grid::O}
51
+ # - *:next_token* is one of {Grid::X} or {Grid::O}
52
+ # - *:row* is one of 1, 2 or 3
53
+ # - *:column* is one of 1, 2 or 3
54
+ # - *:details* is taken verbatim from the :details key of the returned hash of {Evaluator#analyze}
55
+ #
56
+ # @param r [Integer] the row
57
+ # @param c [Integer] the column
58
+ def play(r, c)
59
+ return engine.send_event(:invalid_move, type: :out_of_bounds) unless Grid.contains?(r, c)
60
+ return engine.send_event(:invalid_move, type: :occupied) unless game_context.grid.open?(r, c)
61
+
62
+ game_context.grid[r, c] = game_context.turn
63
+ last_move = { turn: game_context.turn, r: r, c: c }
64
+
65
+ result = engine.evaluator.analyze(game_context.grid, game_context.turn)
66
+
67
+ case result[:status]
68
+ when :ok
69
+ game_context.switch_turns
70
+ engine.send_event(:next_turn, last_move: last_move)
71
+ when :game_over
72
+ case result[:type]
73
+ when :winner
74
+ engine.transition_to_state_and_send_event(
75
+ GameOver,
76
+ :game_over, type: :winner, last_move: last_move, details: result[:details]
77
+ )
78
+ when :squashed
79
+ game_context.switch_turns
80
+ engine.transition_to_state_and_send_event(
81
+ GameOver,
82
+ :game_over, type: :squashed, last_move: last_move
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end