tic_tac_toes 0.0.2 → 0.0.3

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +2 -0
  5. data/Rakefile +2 -2
  6. data/bin/tic_tac_toes +21 -9
  7. data/lib/command_line/menu.rb +11 -12
  8. data/lib/command_line/{io.rb → prompt.rb} +1 -1
  9. data/lib/command_line/runner.rb +18 -18
  10. data/lib/tic_tac_toes/board_factory.rb +9 -0
  11. data/lib/tic_tac_toes/game_state.rb +27 -0
  12. data/lib/tic_tac_toes/game_state_factory.rb +14 -0
  13. data/lib/tic_tac_toes/history.rb +3 -3
  14. data/lib/tic_tac_toes/io.rb +91 -0
  15. data/lib/tic_tac_toes/move_strategies/easy_ai.rb +11 -0
  16. data/lib/tic_tac_toes/move_strategies/hard_ai.rb +59 -0
  17. data/lib/tic_tac_toes/move_strategies/human.rb +18 -0
  18. data/lib/tic_tac_toes/move_strategies/medium_ai.rb +13 -0
  19. data/lib/tic_tac_toes/player.rb +7 -7
  20. data/lib/tic_tac_toes/player_factory.rb +13 -9
  21. data/lib/tic_tac_toes/rules.rb +1 -1
  22. data/lib/tic_tac_toes/strings.rb +3 -2
  23. data/lib/tic_tac_toes/version.rb +1 -1
  24. data/spec/command_line/menu_spec.rb +40 -44
  25. data/spec/command_line/runner_spec.rb +23 -101
  26. data/spec/database/pg_wrapper_spec.rb +0 -1
  27. data/spec/test_board_generator.rb +15 -0
  28. data/spec/tic_tac_toes/board_factory_spec.rb +13 -0
  29. data/spec/tic_tac_toes/board_spec.rb +20 -25
  30. data/spec/tic_tac_toes/game_state_factory_spec.rb +15 -0
  31. data/spec/tic_tac_toes/game_state_spec.rb +66 -0
  32. data/spec/tic_tac_toes/history_spec.rb +3 -4
  33. data/spec/tic_tac_toes/io_spec.rb +163 -0
  34. data/spec/tic_tac_toes/move_strategies/easy_ai_spec.rb +19 -0
  35. data/spec/tic_tac_toes/move_strategies/hard_ai_spec.rb +95 -0
  36. data/spec/tic_tac_toes/move_strategies/human_spec.rb +31 -0
  37. data/spec/tic_tac_toes/move_strategies/medium_ai_spec.rb +21 -0
  38. data/spec/tic_tac_toes/player_factory_spec.rb +13 -16
  39. data/spec/tic_tac_toes/player_spec.rb +14 -16
  40. data/spec/tic_tac_toes/rules_spec.rb +31 -41
  41. data/spec/tic_tac_toes/strings_spec.rb +0 -1
  42. metadata +32 -19
  43. data/lib/tic_tac_toes/easy_ai.rb +0 -9
  44. data/lib/tic_tac_toes/hard_ai.rb +0 -57
  45. data/lib/tic_tac_toes/io_interface.rb +0 -96
  46. data/lib/tic_tac_toes/medium_ai.rb +0 -11
  47. data/spec/tic_tac_toes/easy_ai_spec.rb +0 -20
  48. data/spec/tic_tac_toes/hard_ai_spec.rb +0 -103
  49. data/spec/tic_tac_toes/io_interface_spec.rb +0 -175
  50. data/spec/tic_tac_toes/medium_ai_spec.rb +0 -22
  51. data/spec/tic_tac_toes/spec_helper.rb +0 -13
  52. /data/lib/{tic_tac_toes/tasks → tasks}/destroy_databases.rake +0 -0
  53. /data/lib/{tic_tac_toes/tasks → tasks}/set_up_databases.rake +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eba6ff9bd4e9e32b11a8d56fec6a495e8c3b6f4c
4
- data.tar.gz: 3b3a75a1cce99eaeb7a7f98c816f5b48cc348244
3
+ metadata.gz: de5c575f7ff03180dda1f07d8f1930cc56bde5ca
4
+ data.tar.gz: 7dc353433569c7d289055a4893d37b9fcf845af3
5
5
  SHA512:
6
- metadata.gz: 28447b247bb23d735ccf63dbccacb1a65a5179e382a54fc50ed25ff005d6354552db44b548af93fd26253a2a0055910bcec8798151265a05a177f294483a04f7
7
- data.tar.gz: 9c276bb9a57b910eb7e8a6fac87f5f1036551923c7e682ec19532010fa5cb1fbc6eb68a7001d21b97a3390e7779be9a5f6a519c4967461d86e0b66cb49c77217
6
+ metadata.gz: 435ed2f2a0e102b82302d1ee4fc4c0a617d9e51767fb9976c3fecd2223d210a70915bab7b41047b62a83309fa7c4b3f97533fb90918f5948ad44b3e3350d92da
7
+ data.tar.gz: 4c1b8cfc7d68129cb5516d83807e5660c9bf3a4e23c89a558921cdf0d72b33b59fd9ca1e18f125a8f58a77e00d2420eedd68576cdfb62248b989453a85db0ed5
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.1.1"
4
+ script:
5
+ - bundle exec rake set_up_databases
6
+ - bundle exec rspec
7
+ - bundle exec rake destroy_databases
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tic_tac_toes (0.0.1)
4
+ tic_tac_toes (0.0.2)
5
5
  pg
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/bspatafora/tic_tac_toes.svg?branch=master)](https://travis-ci.org/bspatafora/tic_tac_toes)
2
+
1
3
  # TicTacToes
2
4
 
3
5
  The game Tic-tac-toe, featuring an unbeatable AI.
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
- load './lib/tic_tac_toes/tasks/set_up_databases.rake'
4
- load './lib/tic_tac_toes/tasks/destroy_databases.rake'
3
+ load './lib/tasks/set_up_databases.rake'
4
+ load './lib/tasks/destroy_databases.rake'
data/bin/tic_tac_toes CHANGED
@@ -2,22 +2,34 @@
2
2
 
3
3
  $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../lib')
4
4
 
5
- require 'command_line/io'
5
+ require 'command_line/prompt'
6
+ require 'tic_tac_toes/io'
7
+
8
+ require 'tic_tac_toes/board_factory'
9
+ require 'tic_tac_toes/player_factory'
6
10
  require 'command_line/menu'
7
- require 'database/pg_wrapper'
8
- require 'tic_tac_toes/io_interface'
11
+
9
12
  require 'tic_tac_toes/rules'
13
+
14
+ require 'database/pg_wrapper'
10
15
  require 'tic_tac_toes/history'
16
+ require 'tic_tac_toes/game_state_factory'
11
17
 
12
18
  require 'command_line/runner'
13
19
 
14
20
  database = "tic_tac_toes"
15
21
 
16
- io = CommandLine::IO
17
- io_interface = TicTacToes::IOInterface.new(io)
18
- menu = CommandLine::Menu.new(io_interface)
22
+ prompt = CommandLine::Prompt
23
+ io = TicTacToes::IO.new(prompt)
24
+
25
+ board_factory = TicTacToes::BoardFactory.new
26
+ player_factory = TicTacToes::PlayerFactory.new(io)
27
+ menu = CommandLine::Menu.new(io, board_factory, player_factory)
28
+
29
+ database_wrapper = Database::PGWrapper.new(database)
30
+ history = TicTacToes::History.new(database_wrapper)
31
+ game_state_factory = TicTacToes::GameStateFactory.new(history)
32
+
19
33
  rules = TicTacToes::Rules
20
- database_interface = Database::PGWrapper.new(database)
21
- history = TicTacToes::History.new(database_interface)
22
34
 
23
- CommandLine::Runner.new(io_interface, menu, rules, history).run
35
+ CommandLine::Runner.new(io, menu, game_state_factory, rules).run
@@ -1,16 +1,15 @@
1
- require 'tic_tac_toes/board'
2
- require 'tic_tac_toes/player_factory'
3
1
  require 'tic_tac_toes/rules'
4
2
 
5
3
  module CommandLine
6
4
  class Menu
7
- def initialize(io_interface)
8
- @io_interface = io_interface
9
- @player_factory = TicTacToes::PlayerFactory.new(io_interface)
5
+ def initialize(io, board_factory, player_factory)
6
+ @io = io
7
+ @board_factory = board_factory
8
+ @player_factory = player_factory
10
9
  end
11
10
 
12
11
  def get_board
13
- TicTacToes::Board.new(row_size: get_row_size)
12
+ @board_factory.generate_board(get_row_size)
14
13
  end
15
14
 
16
15
  def get_players
@@ -29,25 +28,25 @@ module CommandLine
29
28
 
30
29
  def get_row_size
31
30
  loop do
32
- row_size = @io_interface.get_row_size
31
+ row_size = @io.get_row_size
33
32
  break row_size if TicTacToes::Rules.row_size_valid?(row_size)
34
- @io_interface.invalid_row_size_error
33
+ @io.invalid_row_size_error
35
34
  end
36
35
  end
37
36
 
38
37
  def get_token(player, taken_tokens)
39
38
  loop do
40
- token = @io_interface.get_token(player)
39
+ token = @io.get_token(player)
41
40
  break token if TicTacToes::Rules.token_valid?(token, taken_tokens)
42
- @io_interface.invalid_token_error
41
+ @io.invalid_token_error
43
42
  end
44
43
  end
45
44
 
46
45
  def get_difficulty
47
46
  loop do
48
- difficulty = @io_interface.get_difficulty
47
+ difficulty = @io.get_difficulty
49
48
  break difficulty if TicTacToes::Rules.difficulty_valid?(difficulty)
50
- @io_interface.invalid_difficulty_error
49
+ @io.invalid_difficulty_error
51
50
  end
52
51
  end
53
52
  end
@@ -1,5 +1,5 @@
1
1
  module CommandLine
2
- module IO
2
+ module Prompt
3
3
  def self.solicit_input
4
4
  gets.chomp
5
5
  end
@@ -1,37 +1,37 @@
1
1
  module CommandLine
2
2
  class Runner
3
- def initialize(io_interface, menu, rules, history)
4
- @io_interface = io_interface
3
+ def initialize(io, menu, game_state_factory, rules)
4
+ @io = io
5
5
  @menu = menu
6
+ @game_state_factory = game_state_factory
6
7
  @rules = rules
7
- @history = history
8
8
  end
9
9
 
10
10
  def run
11
11
  board, players = @menu.get_board, @menu.get_players
12
- @history.record_board_size(board.size)
12
+ game_state = @game_state_factory.generate_game_state(board, players)
13
13
 
14
- take_turn(board, players) until @rules.game_over?(board, players)
15
- end_game(board, players)
14
+ take_turn(game_state) until @rules.game_over?(game_state.board, game_state.players)
15
+ end_game(game_state)
16
16
  end
17
17
 
18
- def take_turn(board, players)
19
- @io_interface.draw_board(board)
20
- @io_interface.thinking_notification if players.first.needs_to_think
18
+ private
21
19
 
22
- move = players.first.make_move(board, players)
23
- @history.record_move(move)
24
- players.rotate!
20
+ def take_turn(game_state)
21
+ @io.draw_board(game_state.board)
22
+ @io.thinking_notification if game_state.current_player.needs_to_think
23
+
24
+ move = game_state.current_player.place_and_return_move(game_state.board, game_state.players)
25
+ game_state.turn_over(move)
25
26
  end
26
27
 
27
- def end_game(board, players)
28
- @io_interface.draw_board(board)
28
+ def end_game(game_state)
29
+ @io.draw_board(game_state.board)
29
30
 
30
- winner = @rules.determine_winner(board, players)
31
+ winner = @rules.determine_winner(game_state.board, game_state.players)
32
+ game_state.game_over(winner)
31
33
 
32
- @history.record_winner(winner)
33
- @history.persist
34
- @io_interface.game_over_notification(winner)
34
+ @io.game_over_notification(winner)
35
35
  end
36
36
  end
37
37
  end
@@ -0,0 +1,9 @@
1
+ require 'tic_tac_toes/board'
2
+
3
+ module TicTacToes
4
+ class BoardFactory
5
+ def generate_board(row_size)
6
+ TicTacToes::Board.new(row_size: row_size)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ module TicTacToes
2
+ class GameState
3
+ attr_reader :board, :players
4
+
5
+ def initialize(board, players, history)
6
+ @board = board
7
+ @players = players
8
+ @history = history
9
+
10
+ @history.record_board_size(@board.size)
11
+ end
12
+
13
+ def current_player
14
+ @players.first
15
+ end
16
+
17
+ def turn_over(move)
18
+ @history.record_move(move)
19
+ @players.rotate!
20
+ end
21
+
22
+ def game_over(winner)
23
+ @history.record_winner(winner)
24
+ @history.persist
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ require 'tic_tac_toes/game_state'
2
+ require 'tic_tac_toes/history'
3
+
4
+ module TicTacToes
5
+ class GameStateFactory
6
+ def initialize(history)
7
+ @history = history
8
+ end
9
+
10
+ def generate_game_state(board, players)
11
+ TicTacToes::GameState.new(board, players, @history)
12
+ end
13
+ end
14
+ end
@@ -2,8 +2,8 @@ module TicTacToes
2
2
  class History
3
3
  attr_reader :board_size, :moves, :winner
4
4
 
5
- def initialize(database_interface)
6
- @database_interface = database_interface
5
+ def initialize(database_wrapper)
6
+ @database_wrapper = database_wrapper
7
7
  end
8
8
 
9
9
  def record_board_size(size)
@@ -21,7 +21,7 @@ module TicTacToes
21
21
  end
22
22
 
23
23
  def persist
24
- @database_interface.record_game_history(self)
24
+ @database_wrapper.record_game_history(self)
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,91 @@
1
+ require 'tic_tac_toes/strings'
2
+
3
+ module TicTacToes
4
+ class IO
5
+ def initialize(io_strategy)
6
+ @io_strategy = io_strategy
7
+ end
8
+
9
+ def get_row_size
10
+ row_size_solicitation
11
+
12
+ Integer(@io_strategy.solicit_input)
13
+ rescue ArgumentError
14
+ not_an_integer_error
15
+ get_row_size
16
+ end
17
+
18
+ def get_token(player)
19
+ token_solicitation(player)
20
+ @io_strategy.solicit_input
21
+ end
22
+
23
+ def get_difficulty
24
+ difficulty_solicitation
25
+ @io_strategy.solicit_input.downcase.to_sym
26
+ end
27
+
28
+ def draw_board(board)
29
+ @io_strategy.display(Strings.board(board))
30
+ end
31
+
32
+ def invalid_row_size_error
33
+ @io_strategy.display_red(Strings::INVALID_ROW_SIZE)
34
+ end
35
+
36
+ def invalid_token_error
37
+ @io_strategy.display_red(Strings::INVALID_TOKEN)
38
+ end
39
+
40
+ def invalid_difficulty_error
41
+ @io_strategy.display_red(Strings::INVALID_DIFFICULTY)
42
+ end
43
+
44
+ def invalid_move_error
45
+ @io_strategy.display_red(Strings::INVALID_MOVE)
46
+ end
47
+
48
+ def thinking_notification
49
+ @io_strategy.display_red(Strings::THINKING)
50
+ end
51
+
52
+ def game_over_notification(winner)
53
+ winner = "Nobody" if winner.nil?
54
+ @io_strategy.display(Strings.game_over_notification(winner))
55
+ end
56
+
57
+ def move_solicitation
58
+ @io_strategy.display(Strings::MOVE_SOLICITATION)
59
+ end
60
+
61
+ def not_an_integer_error
62
+ @io_strategy.display_red(Strings::NOT_AN_INTEGER)
63
+ end
64
+
65
+ def solicit_input
66
+ @io_strategy.solicit_input
67
+ end
68
+
69
+ def red(message)
70
+ @io_strategy.red(message)
71
+ end
72
+
73
+ def blue(message)
74
+ @io_strategy.blue(message)
75
+ end
76
+
77
+ private
78
+
79
+ def row_size_solicitation
80
+ @io_strategy.display(Strings::ROW_SIZE_SOLICITATION)
81
+ end
82
+
83
+ def token_solicitation(player)
84
+ @io_strategy.display(Strings.token_solicitation(player))
85
+ end
86
+
87
+ def difficulty_solicitation
88
+ @io_strategy.display(Strings::DIFFICULTY_SOLICITATION)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,11 @@
1
+ require 'tic_tac_toes/board'
2
+
3
+ module TicTacToes
4
+ module MoveStrategies
5
+ module EasyAI
6
+ def self.move(board, players)
7
+ board.open_spaces.sample
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ require 'tic_tac_toes/rules'
2
+
3
+ module TicTacToes
4
+ module MoveStrategies
5
+ module HardAI
6
+ def self.move(board, players)
7
+ open_spaces = Hash[board.open_spaces.map { |space| [space, nil] }]
8
+
9
+ open_spaces.each do |space, score|
10
+ score = minimax(generate_board(players.first, space, board), :min, players)
11
+ open_spaces[space] = score
12
+ end
13
+
14
+ best_score = open_spaces.values.max
15
+ open_spaces.each { |space, score| return space if score == best_score }
16
+ end
17
+
18
+ def self.minimax(board, current_player, players)
19
+ return score(board, players) if Rules.game_over?(board, players)
20
+
21
+ if current_player == :max
22
+ best_score = -1
23
+ board.open_spaces.each do |space|
24
+ score = minimax(generate_board(players.first, space, board), :min, players)
25
+ best_score = [best_score, score].max
26
+ end
27
+ best_score
28
+
29
+ elsif current_player == :min
30
+ best_score = 1
31
+ board.open_spaces.each do |space|
32
+ score = minimax(generate_board(players.last, space, board), :max, players)
33
+ best_score = [best_score, score].min
34
+ end
35
+ best_score
36
+ end
37
+ end
38
+
39
+ def self.generate_board(player, space, board)
40
+ new_board = Marshal.load(Marshal.dump(board))
41
+ new_board.place(player, space)
42
+ new_board
43
+ end
44
+
45
+ def self.score(board, players)
46
+ own_token, opponent_token = players.first.token, players.last.token
47
+
48
+ case Rules.determine_winner(board, players)
49
+ when own_token
50
+ 1
51
+ when opponent_token
52
+ -1
53
+ else
54
+ 0
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,18 @@
1
+ module TicTacToes
2
+ module MoveStrategies
3
+ class Human
4
+ def initialize(io)
5
+ @io = io
6
+ end
7
+
8
+ def move(board, players)
9
+ @io.move_solicitation
10
+
11
+ Integer(@io.solicit_input)
12
+ rescue ArgumentError
13
+ @io.not_an_integer_error
14
+ move(board, players)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require 'tic_tac_toes/move_strategies/easy_ai'
2
+ require 'tic_tac_toes/move_strategies/hard_ai'
3
+
4
+ module TicTacToes
5
+ module MoveStrategies
6
+ module MediumAI
7
+ def self.move(board, players)
8
+ move_strategy = [EasyAI, HardAI, HardAI].sample
9
+ move_strategy.move(board, players)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,19 +1,19 @@
1
1
  module TicTacToes
2
2
  class Player
3
- attr_reader :decider, :token, :needs_to_think
3
+ attr_reader :move_strategy, :token, :needs_to_think
4
4
 
5
- def initialize(decider, token, needs_to_think, io_interface)
6
- @decider = decider
5
+ def initialize(move_strategy, token, needs_to_think, io)
6
+ @move_strategy = move_strategy
7
7
  @token = token
8
8
  @needs_to_think = needs_to_think
9
- @io_interface = io_interface
9
+ @io = io
10
10
  end
11
11
 
12
- def make_move(board, players)
12
+ def place_and_return_move(board, players)
13
13
  loop do
14
- space = @decider.make_move(board, players)
14
+ space = @move_strategy.move(board, players)
15
15
  break [@token, space] if board.place(self, space)
16
- @io_interface.invalid_move_error
16
+ @io.invalid_move_error
17
17
  end
18
18
  end
19
19
  end
@@ -1,24 +1,28 @@
1
- require 'tic_tac_toes/easy_ai'
2
- require 'tic_tac_toes/hard_ai'
3
- require 'tic_tac_toes/medium_ai'
1
+ require 'tic_tac_toes/move_strategies/human'
2
+ require 'tic_tac_toes/move_strategies/easy_ai'
3
+ require 'tic_tac_toes/move_strategies/medium_ai'
4
+ require 'tic_tac_toes/move_strategies/hard_ai'
4
5
  require 'tic_tac_toes/player'
5
6
 
6
7
  module TicTacToes
7
8
  class PlayerFactory
8
- AI_DIFFICULTIES = { easy: EasyAI, medium: MediumAI, hard: HardAI }
9
-
10
- def initialize(io_interface)
11
- @io_interface = io_interface
9
+ def initialize(io)
10
+ @io = io
12
11
  end
13
12
 
13
+ AIS = {
14
+ easy: ::TicTacToes::MoveStrategies::EasyAI,
15
+ medium: ::TicTacToes::MoveStrategies::MediumAI,
16
+ hard: ::TicTacToes::MoveStrategies::HardAI }
17
+
14
18
  def generate_human_player(token)
15
19
  needs_to_think = false
16
- Player.new(@io_interface, token, needs_to_think, @io_interface)
20
+ Player.new(TicTacToes::MoveStrategies::Human.new(@io), token, needs_to_think, @io)
17
21
  end
18
22
 
19
23
  def generate_computer_player(token, difficulty)
20
24
  needs_to_think = true
21
- Player.new(AI_DIFFICULTIES[difficulty], token, needs_to_think, @io_interface)
25
+ Player.new(AIS[difficulty], token, needs_to_think, @io)
22
26
  end
23
27
  end
24
28
  end
@@ -16,7 +16,7 @@ module TicTacToes
16
16
  end
17
17
 
18
18
  def self.difficulty_valid?(difficulty)
19
- PlayerFactory::AI_DIFFICULTIES.include? difficulty
19
+ PlayerFactory::AIS.include? difficulty
20
20
  end
21
21
 
22
22
  def self.game_over?(board, players)
@@ -1,3 +1,4 @@
1
+ require 'command_line/prompt'
1
2
  require 'tic_tac_toes/rules'
2
3
 
3
4
  module TicTacToes
@@ -71,9 +72,9 @@ module TicTacToes
71
72
 
72
73
  def self.get_colored_token(player)
73
74
  if player.needs_to_think
74
- return CommandLine::IO.red(player.token)
75
+ return CommandLine::Prompt.red(player.token)
75
76
  else
76
- return CommandLine::IO.blue(player.token)
77
+ return CommandLine::Prompt.blue(player.token)
77
78
  end
78
79
  end
79
80
 
@@ -1,3 +1,3 @@
1
1
  module TicTacToes
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end