xo 0.0.1 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f4e9916b4a489801329e2b8713a883a06ebef55f
4
- data.tar.gz: 83ea35a8950526c5ab4a10587ac151ce3523f6d6
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTMyNzA0ODhkYmI4OGE5ZjM3ZDc3YjZkNDA2NGQ0ZWZjMWI3ODYwYQ==
5
+ data.tar.gz: !binary |-
6
+ OWNlNjMxM2JmMDEzMTQ0YWJjNjdjMDUzYmQ0ZDQ3YmY3OWUzMDk4OA==
5
7
  SHA512:
6
- metadata.gz: ff7f19ed461e17028efaeda45010b6faf967103640d004b80da343f179dd4dbf307577aeebfb38bcfeda61f33c01b45657ea5ceadd911c7ca134496d6ff532b5
7
- data.tar.gz: e31c7c082b239cd9e3efc10edb36e9e92197e383b4da15f84f8a9d6520fa55c72ccbaa743d3e0aa1fe224f4260bf2c8e4327b57d564eaa1aea8148dd310aa06b
8
+ metadata.gz: !binary |-
9
+ OGY3M2IyNWE0Y2YzYzZkMzlhMmEzNzIyODg3YTFiNzg1MzFiMjZkZDBiMjZl
10
+ OWMzYzllZjVhYTVjOWM1NTYxY2MyYWQ3ODdlZDhmNzUxMjNlNTA0N2VhYzQ2
11
+ MjE4OWJhNGE0NTk3YTFiYTU3MWViMjAxODcxZGQ0OGFiYjAwMGU=
12
+ data.tar.gz: !binary |-
13
+ ZjllZDA0YzY0MDYzMTVlY2EzMzlkZmE4NjU2ZjEzNDYwOTJiM2EwNDYyNmI0
14
+ YTRlZWRlOWM0N2MyNjFkM2FhYmU0MTJlNzA5NDVhMjFmZjk0OTQ0NTMzYzYx
15
+ Yzk2ZjFmNzAyY2JlNzY0OWM3OTEyMTNkYjkyOTBiMTcwNGJjZDQ=
data/.gitignore CHANGED
@@ -1 +1,5 @@
1
1
  *.gem
2
+ .ruby-gemset
3
+ .ruby-version
4
+ .yardoc
5
+ /coverage
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ xo (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coveralls (0.7.0)
10
+ multi_json (~> 1.3)
11
+ rest-client
12
+ simplecov (>= 0.7)
13
+ term-ansicolor
14
+ thor
15
+ docile (1.1.3)
16
+ mime-types (2.2)
17
+ minitest (5.3.3)
18
+ multi_json (1.10.0)
19
+ rake (10.3.1)
20
+ redcarpet (3.1.1)
21
+ rest-client (1.6.7)
22
+ mime-types (>= 1.16)
23
+ simplecov (0.8.2)
24
+ docile (~> 1.1.0)
25
+ multi_json
26
+ simplecov-html (~> 0.8.0)
27
+ simplecov-html (0.8.0)
28
+ term-ansicolor (1.3.0)
29
+ tins (~> 1.0)
30
+ thor (0.19.1)
31
+ tins (1.1.0)
32
+ yard (0.8.7.4)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ coveralls (~> 0.7)
39
+ minitest (~> 5.3)
40
+ rake (~> 10.3)
41
+ redcarpet (~> 3.1)
42
+ xo!
43
+ yard (~> 0.8)
data/README.md CHANGED
@@ -1,55 +1,120 @@
1
1
  # xo
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/xo.svg)](http://badge.fury.io/rb/xo)
4
+ [![Build Status](https://travis-ci.org/dwayne/xo.svg?branch=master)](https://travis-ci.org/dwayne/xo)
5
+ [![Coverage Status](https://coveralls.io/repos/dwayne/xo/badge.png)](https://coveralls.io/r/dwayne/xo)
6
+ [![Code Climate](https://codeclimate.com/github/dwayne/xo.png)](https://codeclimate.com/github/dwayne/xo)
7
+
3
8
  A [Ruby](http://www.ruby-lang.org/en/) library for [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
4
9
 
5
- # Performance of the Minimax Algorithm
10
+ The code is well documented and fully tested, so please have a [read of the documentation](http://rubydoc.info/github/dwayne/xo) and have a [look at the tests](https://github.com/dwayne/xo/tree/master/spec/xo).
11
+
12
+ My implementation of the [Minimax algorithm](http://en.wikipedia.org/wiki/Minimax#Minimax_algorithm_with_alternate_moves) might be a little different than what you've seen before. It uses symmetry to significantly reduce the search space and in so doing we get good [performance out of the algorithm](#Performance_of_the_Minimax_Algorithm). However, I still want to get it under 1 second. I'd love to get your thoughts on how I can make it happen. Have a [look](https://github.com/dwayne/xo/blob/master/lib/xo/ai/minimax.rb#L23).
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ $ gem install xo
18
+ ```
19
+
20
+ ## Example usage
21
+
22
+ Managing the grid yourself.
6
23
 
7
24
  ```ruby
8
- require 'benchmark'
9
25
  require 'xo'
10
26
 
11
- # Empty grid
12
- g = XO::Grid.new
27
+ include XO
28
+
29
+ g = Grid.new('xx oo')
30
+
31
+ puts g # => x | x |
32
+ # ---+---+---
33
+ # o | o |
34
+ # ---+---+---
35
+ # | |
36
+
37
+ Evaluator.analyze(g, Grid::X) # => { status: :ok }
38
+
39
+ g[1, 3] = Grid::X
40
+ Evaluator.analyze(g, Grid::X) # => { status: :game_over,
41
+ # type: :winner,
42
+ # details: [{
43
+ # where: :row,
44
+ # index: 1,
45
+ # positions: [[1, 1], [1, 2], [1, 3]]
46
+ # }]
47
+ # }
48
+
49
+ Evaluator.analyze(g, Grid::O) # => { status: :game_over,
50
+ # type: :loser,
51
+ # details: [{
52
+ # where: :row,
53
+ # index: 1,
54
+ # positions: [[1, 1], [1, 2], [1, 3]]
55
+ # }]
56
+ # }
57
+ ```
58
+
59
+ The problem with managing the grid yourself is that there is nothing stopping you from making bad moves. For example, playing twice.
60
+
61
+ ```ruby
62
+ g = Grid.new('xx')
63
+ Evaluator.analyze(g, Grid::O) # => { status: :invalid_grid,
64
+ # type: :too_many_moves_ahead
65
+ # }
66
+ ```
13
67
 
14
- puts Benchmark.measure { XO::AI.minimax(g, :x) }
15
- # => 0.000000 0.000000 0.000000 ( 0.000463)
16
- # => O(1) time due to caching
68
+ To avoid such situations, let the engine handle game play. Once you tell it who plays first, then it ensures that the game play follows the rules of [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
17
69
 
18
- # One spot taken
19
- g[1, 1] = :x
70
+ ```ruby
71
+ e = Engine.new
72
+ e.start(Grid::O).play(2, 1).play(1, 1).play(2, 2).play(1, 2).play(2, 3)
73
+ e.last_event # => { name: :game_over,
74
+ # type: :winner,
75
+ # last_move: { turn: :o, r: 2, c: 3 },
76
+ # details: [{
77
+ # where: :row,
78
+ # index: 2,
79
+ # positions: [[2, 1], [2, 2], [2, 3]]
80
+ # }]
81
+ # }
82
+ ```
20
83
 
21
- puts Benchmark.measure { XO::AI.minimax(g, :o) }
22
- # => 0.000000 0.000000 0.000000 ( 0.000216)
23
- # => O(1) time due to caching
84
+ I also built a [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe) command-line client that uses the library. See how everything comes together by viewing its implementation right [here](https://github.com/dwayne/xo/blob/master/bin/xo).
24
85
 
25
- # Two spots taken
26
- g[1, 3] = :o
86
+ To use the [client](https://github.com/dwayne/xo/blob/master/bin/xo) just type,
27
87
 
28
- puts Benchmark.measure { XO::AI.minimax(g, :x) }
29
- # => 0.690000 0.000000 0.690000 ( 0.695095)
30
- # => Worst-case time, performance only improves from here on as the grid gets filled
88
+ ```
89
+ $ xo
31
90
  ```
32
91
 
33
- # Testing
92
+ on the command-line after installing the gem.
34
93
 
35
- You can run:
94
+ ## Performance of the Minimax Algorithm
36
95
 
37
- - All tests: `rake test`
38
- - One test: `ruby -Ilib -Ispec spec/path_to_spec_file.rb`
96
+ ```ruby
97
+ require 'benchmark'
98
+ require 'xo/ai/minimax'
99
+
100
+ puts Benchmark.measure { XO::AI::Minimax.instance }
101
+ # => 3.090000 0.000000 3.090000 ( 3.091686)
102
+ ```
103
+
104
+ ## Testing
39
105
 
40
- # TODO
106
+ You can run:
41
107
 
42
- 1. Write documentation.
43
- 2. Show example usage.
44
- 3. Write an example Tic-tac-toe command-line game client.
108
+ - All specs: `bundle exec rake`, or
109
+ - A specific spec: `bundle exec ruby -Ilib -Ispec spec/path_to_spec_file.rb`
45
110
 
46
- # Contributing
111
+ ## Contributing
47
112
 
48
113
  If you'd like to contribute a feature or bugfix: Thanks! To make sure your fix/feature has a high chance of being included, please read the following guidelines:
49
114
 
50
115
  1. Post a [pull request](https://github.com/dwayne/xo/compare/).
51
116
  2. Make sure there are tests! I will not accept any patch that is not tested. It's a rare time when explicit tests aren't needed. If you have questions about writing tests for xo, please open a [GitHub issue](https://github.com/dwayne/xo/issues/new).
52
117
 
53
- # License
118
+ ## License
54
119
 
55
120
  xo is Copyright © 2014 Dwayne R. Crooks. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
data/Rakefile CHANGED
@@ -5,3 +5,5 @@ Rake::TestTask.new do |t|
5
5
  t.pattern = 'spec/**/*_spec.rb'
6
6
  t.libs.push 'spec'
7
7
  end
8
+
9
+ task default: :test
data/bin/xo ADDED
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # A Tic-tac-toe game client based on xo.
4
+
5
+ require 'xo'
6
+ include XO
7
+
8
+ AUTHOR = "Dwayne Crooks"
9
+ EMAIL = "me@dwaynecrooks.com"
10
+
11
+ class Player
12
+
13
+ def moves(grid, turn)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ private
18
+
19
+ def all_open_moves(grid)
20
+ moves = []
21
+
22
+ grid.each_open do |r, c|
23
+ moves << [r, c]
24
+ end
25
+
26
+ moves
27
+ end
28
+
29
+ def all_smart_moves(grid, turn)
30
+ AI::Minimax.instance.moves(grid, turn)
31
+ end
32
+ end
33
+
34
+ class Easy < Player
35
+
36
+ def moves(grid, turn)
37
+ all_open_moves(grid)
38
+ end
39
+ end
40
+
41
+ class Medium < Player
42
+
43
+ def moves(grid, turn)
44
+ smart_moves = all_smart_moves(grid, turn)
45
+ dumb_moves = all_open_moves(grid) - smart_moves
46
+
47
+ dumb_moves.empty? ? smart_moves : (rand < 0.75 ? smart_moves : dumb_moves)
48
+ end
49
+ end
50
+
51
+ class Hard < Player
52
+
53
+ def moves(grid, turn)
54
+ all_smart_moves(grid, turn)
55
+ end
56
+ end
57
+
58
+ class Human
59
+
60
+ def initialize(humans)
61
+ @humans = humans
62
+ end
63
+
64
+ def get_move(grid, turn)
65
+ puts grid
66
+ puts
67
+
68
+ puts "Enter your move in the format r, c."
69
+ prompt_for_move(grid, turn)
70
+ end
71
+
72
+ def handle_next_turn(grid)
73
+ puts grid
74
+ puts
75
+ end
76
+
77
+ def handle_game_over(grid, event)
78
+ case event[:type]
79
+ when :winner
80
+ puts "Congratulations! #{@humans == 2 ? event[:last_move][:turn] : 'You'} won."
81
+ when :squashed
82
+ puts "Sorry! Game squashed."
83
+ end
84
+
85
+ puts
86
+ end
87
+
88
+ def handle_invalid_move(event)
89
+ puts "Sorry! You cannot make that move."
90
+
91
+ case event[:type]
92
+ when :occupied
93
+ puts "That position is occupied."
94
+ when :out_of_bounds
95
+ puts "That position is not on the board."
96
+ end
97
+
98
+ puts
99
+ end
100
+
101
+ private
102
+
103
+ def prompt_for_move(grid, turn)
104
+ print "> "
105
+
106
+ position = gets.chomp.split(',').map(&:strip).map(&:to_i)
107
+
108
+ if position.length == 2
109
+ puts
110
+ position
111
+ else
112
+ prompt_for_move(grid, turn)
113
+ end
114
+ end
115
+ end
116
+
117
+ class Computer
118
+
119
+ def initialize(humans, ai)
120
+ @humans = humans
121
+ @ai = ai
122
+ end
123
+
124
+ def get_move(grid, turn)
125
+ puts "Waiting for the computer to play..." if @humans == 1
126
+
127
+ r, c = @ai.moves(grid, turn).shuffle[0]
128
+
129
+ if @humans == 1
130
+ puts "The computer played at #{r}, #{c}."
131
+ puts
132
+ end
133
+
134
+ [r, c]
135
+ end
136
+
137
+ def handle_next_turn(grid)
138
+ end
139
+
140
+ def handle_game_over(grid, event)
141
+ if @humans == 0
142
+ case event[:type]
143
+ when :winner
144
+ puts "#{event[:last_move][:turn]} wins!"
145
+ when :squashed
146
+ puts "Game squashed!"
147
+ end
148
+ elsif @humans == 1
149
+ puts grid
150
+ puts
151
+
152
+ case event[:type]
153
+ when :winner
154
+ puts "Sorry! You lost."
155
+ when :squashed
156
+ puts "Sorry! Game squashed."
157
+ end
158
+
159
+ puts
160
+ end
161
+ end
162
+
163
+ def handle_invalid_move(event)
164
+ end
165
+ end
166
+
167
+ class Game
168
+
169
+ def initialize(players)
170
+ @players = players
171
+ @engine = Engine.new
172
+ end
173
+
174
+ def start(turn = false)
175
+ if turn
176
+ @engine.start(turn)
177
+ else
178
+ event = @engine.last_event
179
+
180
+ case event[:type]
181
+ when :winner
182
+ @engine.continue_playing(@engine.turn)
183
+ when :squashed
184
+ @engine.continue_playing(@engine.next_turn)
185
+ end
186
+ end
187
+ end
188
+
189
+ def run
190
+ run_one_turn while @engine.state != :game_over
191
+ end
192
+
193
+ private
194
+
195
+ def run_one_turn
196
+ turn = @engine.turn
197
+ player = @players[turn]
198
+
199
+ r, c = player.get_move(@engine.grid, turn)
200
+
201
+ event = @engine.play(r, c).last_event
202
+
203
+ case event[:name]
204
+ when :next_turn
205
+ player.handle_next_turn(@engine.grid)
206
+ when :game_over
207
+ player.handle_game_over(@engine.grid, event)
208
+ when :invalid_move
209
+ player.handle_invalid_move(event)
210
+ end
211
+ end
212
+ end
213
+
214
+ def welcome
215
+ heading = "Welcome to xo! A Tic-tac-toe game client created by #{AUTHOR}."
216
+
217
+ puts heading
218
+ puts "=" * heading.length
219
+ puts
220
+
221
+ puts "NOTE: You can exit the game at anytime by pressing CTRL-C."
222
+ puts
223
+ end
224
+
225
+ def ask_for_difficulty_level
226
+ puts "Please select your difficulty level:"
227
+ puts "1. Easy"
228
+ puts "2. Medium"
229
+ puts "3. Hard"
230
+
231
+ get_option({ '1' => Easy, '2' => Medium, '3' => Hard })
232
+ end
233
+
234
+ def ask_for_token
235
+ puts "Do you want to play as x or o?"
236
+
237
+ get_option({ 'x' => Grid::X, 'o' => Grid::O })
238
+ end
239
+
240
+ def ask_to_play_first
241
+ puts "Do you want to play first (y or n)?"
242
+
243
+ get_option({ 'y' => true, 'n' => false })
244
+ end
245
+
246
+ def ask_to_play_again
247
+ puts "Do you want to play again (y or n)?"
248
+
249
+ get_option({ 'y' => true, 'n' => false })
250
+ end
251
+
252
+ def get_option(options)
253
+ print "> "
254
+
255
+ selection = gets.chomp
256
+
257
+ if options.key?(selection)
258
+ puts
259
+ options[selection]
260
+ else
261
+ get_option(options)
262
+ end
263
+ end
264
+
265
+ def say_goodbye
266
+ puts "Thank you for playing."
267
+ puts "Bye!"
268
+ end
269
+
270
+ def terminate
271
+ puts
272
+ puts
273
+ say_goodbye
274
+ exit
275
+ end
276
+
277
+ def set_up_ctrl_c_handler
278
+ trap("INT") {
279
+ terminate
280
+ }
281
+ end
282
+
283
+ def main
284
+ set_up_ctrl_c_handler
285
+
286
+ welcome
287
+
288
+ level = ask_for_difficulty_level
289
+
290
+ human_token = ask_for_token
291
+ computer_token = Grid.other_token(human_token)
292
+
293
+ play_first = ask_to_play_first
294
+
295
+ players = {}
296
+ players[human_token] = Human.new(1)
297
+ players[computer_token] = Computer.new(1, level.new)
298
+
299
+ game = Game.new(players)
300
+ game.start(play_first ? human_token : computer_token)
301
+
302
+ loop do
303
+ game.run
304
+
305
+ play_again = ask_to_play_again
306
+ break unless play_again
307
+
308
+ game.start
309
+ end
310
+
311
+ say_goodbye
312
+ end
313
+
314
+ main