xo 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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