ttt 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.
- data/Gemfile +6 -0
- data/Gemfile.lock +51 -0
- data/MIT-License.md +8 -0
- data/Rakefile +324 -0
- data/Readme.md +35 -0
- data/bin/ttt +6 -0
- data/features/binary.feature +39 -0
- data/features/computer_player.feature +62 -0
- data/features/create_game.feature +63 -0
- data/features/finish_game.feature +53 -0
- data/features/finished_states.feature +56 -0
- data/features/mark_board.feature +24 -0
- data/features/step_definitions/binary_steps.rb +42 -0
- data/features/step_definitions/ttt_steps.rb +82 -0
- data/features/support/env.rb +8 -0
- data/features/view_board_as_developer.feature +13 -0
- data/features/view_board_as_tester.feature +9 -0
- data/lib/ttt.rb +1 -0
- data/lib/ttt/binary.rb +96 -0
- data/lib/ttt/computer_player.rb +74 -0
- data/lib/ttt/game.rb +129 -0
- data/lib/ttt/interface.rb +22 -0
- data/lib/ttt/interface/cli.rb +95 -0
- data/lib/ttt/interface/cli/players.rb +56 -0
- data/lib/ttt/interface/cli/views.rb +124 -0
- data/lib/ttt/interface/limelight.rb +18 -0
- data/lib/ttt/interface/limelight/players/restart_as_first.rb +5 -0
- data/lib/ttt/interface/limelight/players/restart_as_second.rb +6 -0
- data/lib/ttt/interface/limelight/players/square.rb +105 -0
- data/lib/ttt/interface/limelight/props.rb +10 -0
- data/lib/ttt/interface/limelight/styles.rb +101 -0
- data/lib/ttt/ratings.rb +849 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/ttt/computer_player_spec.rb +86 -0
- data/spec/ttt/game_spec.rb +294 -0
- data/spec/ttt/rating_spec.rb +75 -0
- metadata +139 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ttt/ratings'
|
2
|
+
|
3
|
+
module TTT
|
4
|
+
class ComputerPlayer
|
5
|
+
|
6
|
+
attr_accessor :game
|
7
|
+
|
8
|
+
def initialize(game)
|
9
|
+
self.game = game
|
10
|
+
end
|
11
|
+
|
12
|
+
def player_number
|
13
|
+
game.turn
|
14
|
+
end
|
15
|
+
|
16
|
+
def take_turn
|
17
|
+
game.mark best_move
|
18
|
+
end
|
19
|
+
|
20
|
+
def best_move
|
21
|
+
return imperative_move if imperative_move? # allow to customize certain situations
|
22
|
+
move, rating, game = moves_by_rating.first # otherwise go by rating
|
23
|
+
move
|
24
|
+
end
|
25
|
+
|
26
|
+
def moves_by_rating
|
27
|
+
return to_enum(:moves_by_rating) unless block_given?
|
28
|
+
moves = []
|
29
|
+
game.available_moves.each do |move|
|
30
|
+
new_game = game.pristine_mark move
|
31
|
+
moves << [ move, rate(new_game), new_game ]
|
32
|
+
end
|
33
|
+
moves = moves.sort_by { |move, rating, new_game| -rating } # highest rating first
|
34
|
+
moves.each { |move, rating, new_game| yield move, rating, new_game }
|
35
|
+
end
|
36
|
+
|
37
|
+
def rate(game)
|
38
|
+
RATINGS[game.board][player_number]
|
39
|
+
end
|
40
|
+
|
41
|
+
# allows us to override ratings in cases where they make the robot look stupid
|
42
|
+
def imperative_move
|
43
|
+
# if we can win *this turn*, then take it because
|
44
|
+
# it rates winning next turn the same as winning in 3 turns
|
45
|
+
game.available_moves.each do |move|
|
46
|
+
new_game = game.pristine_mark move
|
47
|
+
return move if new_game.over? && new_game.winner == player_number
|
48
|
+
end
|
49
|
+
|
50
|
+
# if we can block the opponent from winning *this turn*, then take it, because
|
51
|
+
# it rates losing this turn the same as losing in 3 turns
|
52
|
+
if moves_by_rating.all? { |move, rating, game| rating == -1 }
|
53
|
+
Game.winning_states do |position1, position2, position3|
|
54
|
+
a, b, c = board[position1-1, 1].to_i, board[position2-1, 1].to_i, board[position3-1, 1].to_i
|
55
|
+
if a + b + c == opponent_number * 2
|
56
|
+
return a.zero? ? position1 : b.zero? ? position2 : position3
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def imperative_move?
|
63
|
+
!!imperative_move
|
64
|
+
end
|
65
|
+
|
66
|
+
def board
|
67
|
+
game.board
|
68
|
+
end
|
69
|
+
|
70
|
+
def opponent_number
|
71
|
+
player_number == 1 ? 2 : 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/ttt/game.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
module TTT
|
2
|
+
class Game
|
3
|
+
|
4
|
+
DEFAULT_BOARD = '0'*9
|
5
|
+
attr_writer :board
|
6
|
+
|
7
|
+
def initialize(board=DEFAULT_BOARD)
|
8
|
+
self.board = board.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def turn
|
12
|
+
return if over?
|
13
|
+
board.scan('1').size - board.scan('2').size + 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def mark(position)
|
17
|
+
board[position-1] = turn.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def board(style=nil)
|
21
|
+
return @board unless style
|
22
|
+
" %s | %s | %s \n----|---|----\n %s | %s | %s \n----|---|----\n %s | %s | %s " % @board.gsub('0', ' ').split('')
|
23
|
+
end
|
24
|
+
|
25
|
+
def over?
|
26
|
+
winner || board.split(//).all? { |char| char == '1' || char == '2' }
|
27
|
+
end
|
28
|
+
|
29
|
+
def status(player_number)
|
30
|
+
return nil unless over?
|
31
|
+
(winner == player_number) ? :wins :
|
32
|
+
winner ? :loses :
|
33
|
+
:ties
|
34
|
+
end
|
35
|
+
|
36
|
+
def tie?
|
37
|
+
over? && !winner
|
38
|
+
end
|
39
|
+
|
40
|
+
def winner
|
41
|
+
return if winning_positions.empty?
|
42
|
+
self[winning_positions.first]
|
43
|
+
end
|
44
|
+
|
45
|
+
def available_moves
|
46
|
+
return [] if over?
|
47
|
+
to_return = []
|
48
|
+
board.split(//).each_with_index do |char, index|
|
49
|
+
to_return << index.next if char != '1' && char != '2'
|
50
|
+
end
|
51
|
+
to_return
|
52
|
+
end
|
53
|
+
|
54
|
+
def pristine_mark(position)
|
55
|
+
marked = self.class.new board.dup
|
56
|
+
marked.mark position
|
57
|
+
marked
|
58
|
+
end
|
59
|
+
|
60
|
+
def winning_states(&block)
|
61
|
+
self.class.winning_states(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def winning_positions
|
65
|
+
winning_states do |pos1, pos2, pos3|
|
66
|
+
next unless board[pos1, 1] == board[pos2, 1]
|
67
|
+
next unless board[pos1, 1] == board[pos3, 1]
|
68
|
+
next unless board[pos1, 1] =~ /^(1|2)$/
|
69
|
+
return [pos1+1, pos2+1, pos3+1]
|
70
|
+
end
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
|
74
|
+
def [](position)
|
75
|
+
player = board[position-1, 1].to_i
|
76
|
+
return player if player == 1 || player == 2
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
module TTT
|
85
|
+
class << Game
|
86
|
+
def congruent?(board1, board2)
|
87
|
+
each_congruent(board2).any? { |congruent| board1 == congruent }
|
88
|
+
end
|
89
|
+
|
90
|
+
def each_congruent(board)
|
91
|
+
return to_enum(:each_congruent, board) unless block_given?
|
92
|
+
each_rotation(board) { |congruent| yield congruent }
|
93
|
+
each_rotation(reflect_board board) { |congruent| yield congruent }
|
94
|
+
end
|
95
|
+
|
96
|
+
def reflect_board(board)
|
97
|
+
board = board.dup
|
98
|
+
board[0..2], board[6..8] = board[6..8], board[0..2]
|
99
|
+
board
|
100
|
+
end
|
101
|
+
|
102
|
+
def each_rotation(board)
|
103
|
+
return to_enum(:each_rotation, board) unless block_given?
|
104
|
+
board = board.dup
|
105
|
+
4.times do
|
106
|
+
yield board.dup
|
107
|
+
board = rotate_board(board)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def rotate_board(board)
|
112
|
+
board = board.dup
|
113
|
+
board[0], board[1], board[2], board[3], board[5], board[6], board[7], board[8] =
|
114
|
+
board[6], board[3], board[0], board[7], board[1], board[8], board[5], board[2]
|
115
|
+
board
|
116
|
+
end
|
117
|
+
|
118
|
+
def winning_states
|
119
|
+
yield 0, 1, 2
|
120
|
+
yield 3, 4, 5
|
121
|
+
yield 6, 7, 8
|
122
|
+
yield 0, 3, 6
|
123
|
+
yield 1, 4, 7
|
124
|
+
yield 2, 5, 8
|
125
|
+
yield 0, 4, 8
|
126
|
+
yield 2, 4, 6
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module TTT
|
2
|
+
module Interface
|
3
|
+
def self.registered
|
4
|
+
@registered_interfaces ||= Hash.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.registered?(name)
|
8
|
+
registered.has_key? name
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.registered_names
|
12
|
+
registered.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.register(name, interface)
|
16
|
+
registered[name] = interface
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'ttt/interface/cli'
|
22
|
+
require 'ttt/interface/limelight'
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'ttt/interface/cli/players'
|
2
|
+
require 'ttt/interface/cli/views'
|
3
|
+
|
4
|
+
module TTT
|
5
|
+
module Interface
|
6
|
+
class CLI
|
7
|
+
Interface.register 'cli', self
|
8
|
+
|
9
|
+
X = 'X'
|
10
|
+
O = 'O'
|
11
|
+
|
12
|
+
attr_accessor :game, :filein, :fileout, :fileerr, :player1, :player2, :turn
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
self.filein = options.fetch :filein, $stdin
|
16
|
+
self.fileout = options.fetch :fileout, $stdout
|
17
|
+
self.fileerr = options.fetch :fileerr, $stderr
|
18
|
+
self.turn = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def play
|
22
|
+
fileout.puts "Welcome to Tic Tac Toe"
|
23
|
+
fileout.flush
|
24
|
+
create_game
|
25
|
+
create_player1
|
26
|
+
create_player2
|
27
|
+
until game.over?
|
28
|
+
display_board
|
29
|
+
take_current_turn
|
30
|
+
end
|
31
|
+
display_results
|
32
|
+
end
|
33
|
+
|
34
|
+
def display_results
|
35
|
+
display_board
|
36
|
+
if game.tie?
|
37
|
+
puts "The game ended in a tie."
|
38
|
+
else
|
39
|
+
puts "Player #{game.winner} won the game."
|
40
|
+
end
|
41
|
+
puts "Play again soon :)"
|
42
|
+
end
|
43
|
+
|
44
|
+
def take_current_turn
|
45
|
+
current_player.take_turn
|
46
|
+
self.turn += 1
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_player
|
50
|
+
turn.even? ? player1 : player2
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_game
|
54
|
+
self.game = Game.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_player1
|
58
|
+
self.player1 = create_player X, 'first'
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_player2
|
62
|
+
self.player2 = create_player O, 'second'
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_player(letter, position)
|
66
|
+
type = prompt "#{letter} will go #{position}, would you like #{letter} to be a human or a computer? (h/c) ", :validate => /^[hc]$/i
|
67
|
+
if type =~ /c/i
|
68
|
+
ComputerPlayer.new game, self, letter
|
69
|
+
else
|
70
|
+
HumanPlayer.new game, self, letter
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def prompt(message, validation={})
|
75
|
+
validation[:validate] ||= //
|
76
|
+
fileout.print message
|
77
|
+
input = filein.gets
|
78
|
+
until input =~ validation[:validate]
|
79
|
+
fileout.puts "Invalid, input."
|
80
|
+
fileout.print message
|
81
|
+
input = filein.gets
|
82
|
+
end
|
83
|
+
input
|
84
|
+
end
|
85
|
+
|
86
|
+
def display_board
|
87
|
+
Views.new(self).display_board
|
88
|
+
end
|
89
|
+
|
90
|
+
def board
|
91
|
+
game.board
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'ttt/computer_player'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module TTT
|
5
|
+
module Interface
|
6
|
+
class CLI
|
7
|
+
|
8
|
+
|
9
|
+
class Player
|
10
|
+
attr_accessor :game, :cli, :marker
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :cli, :fileout, :filein, :fileerr, :prompt
|
14
|
+
|
15
|
+
def initialize(game, cli, marker)
|
16
|
+
self.game = game
|
17
|
+
self.cli = cli
|
18
|
+
self.marker = marker
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
class ComputerPlayer < Player
|
24
|
+
attr_accessor :computer
|
25
|
+
def initialize(*args)
|
26
|
+
super
|
27
|
+
self.computer = TTT::ComputerPlayer.new game
|
28
|
+
end
|
29
|
+
def take_turn
|
30
|
+
computer.take_turn
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
class HumanPlayer <Player
|
36
|
+
def take_turn
|
37
|
+
fileout.puts "The nine squares consecutively map to a number. "\
|
38
|
+
"Topleft starts at 1, topright continues with 3, and bottomright ends with 9."
|
39
|
+
move = prompt "Where would you like to move? (#{list_available_moves}) ", :validate => available_moves_regex
|
40
|
+
game.mark move.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_available_moves
|
44
|
+
game.available_moves.join(', ')
|
45
|
+
end
|
46
|
+
|
47
|
+
def available_moves_regex
|
48
|
+
moves = game.available_moves
|
49
|
+
/^[#{moves.join}]$/
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'ttt/interface/cli/players'
|
2
|
+
require 'ttt/interface/cli/views'
|
3
|
+
|
4
|
+
module TTT
|
5
|
+
module Interface
|
6
|
+
class CLI
|
7
|
+
class Views
|
8
|
+
|
9
|
+
attr_accessor :cli
|
10
|
+
|
11
|
+
def initialize(cli)
|
12
|
+
self.cli = cli
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(meth, *args, &block)
|
16
|
+
super unless cli.respond_to? meth
|
17
|
+
cli.send meth, *args, &block
|
18
|
+
end
|
19
|
+
|
20
|
+
def display_board
|
21
|
+
fileout.print row(0) << horizontal_separator <<
|
22
|
+
row(1) << horizontal_separator <<
|
23
|
+
row(2) << "\n\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
def row(n)
|
27
|
+
n *= 3
|
28
|
+
line(n) << line(n+1) << line(n+2)
|
29
|
+
end
|
30
|
+
|
31
|
+
def line(n)
|
32
|
+
offset, col = (n/3)*3, (n%3)
|
33
|
+
square(offset+1, col) << vertical_separator <<
|
34
|
+
square(offset+2, col) << vertical_separator <<
|
35
|
+
square(offset+3, col) << "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def horizontal_separator
|
39
|
+
"-----|-----|-----\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def vertical_separator
|
43
|
+
"|"
|
44
|
+
end
|
45
|
+
|
46
|
+
def square(num, line)
|
47
|
+
send "line#{line}_for", num
|
48
|
+
end
|
49
|
+
|
50
|
+
def line0_for(square)
|
51
|
+
if forward_diagonal_winner?(square)
|
52
|
+
"\\ "
|
53
|
+
elsif backward_diagonal_winner?(square)
|
54
|
+
" /"
|
55
|
+
elsif vertical_winner? square
|
56
|
+
" | "
|
57
|
+
else
|
58
|
+
" "
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def line1_for(square)
|
63
|
+
if horizontal_winner? square
|
64
|
+
"--%s--"
|
65
|
+
else
|
66
|
+
" %s "
|
67
|
+
end % char_for(square)
|
68
|
+
end
|
69
|
+
|
70
|
+
def line2_for(square)
|
71
|
+
if forward_diagonal_winner?(square)
|
72
|
+
" \\"
|
73
|
+
elsif backward_diagonal_winner?(square)
|
74
|
+
"/ "
|
75
|
+
elsif vertical_winner? square
|
76
|
+
" | "
|
77
|
+
else
|
78
|
+
" "
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def winner?(square)
|
83
|
+
game.over? && !game.tie? && game.winning_positions.include?(square)
|
84
|
+
end
|
85
|
+
|
86
|
+
def forward_diagonal_winner?(square)
|
87
|
+
return false unless winner? square
|
88
|
+
[[1, 5], [5, 9], [9, 5]].any? do |s1, s2|
|
89
|
+
square == s1 && winner?(s2)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def backward_diagonal_winner?(square)
|
94
|
+
return false unless winner? square
|
95
|
+
[[3, 5], [5, 7], [7, 5]].any? do |s1, s2|
|
96
|
+
square == s1 && winner?(s2)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def vertical_winner?(square)
|
101
|
+
return false unless winner? square
|
102
|
+
winner? (square + 2) % 9 + 1
|
103
|
+
end
|
104
|
+
|
105
|
+
def horizontal_winner?(square)
|
106
|
+
return false unless winner? square
|
107
|
+
winner?(square+1) || winner?(square-1)
|
108
|
+
end
|
109
|
+
|
110
|
+
def char_for(position)
|
111
|
+
case game[position]
|
112
|
+
when nil
|
113
|
+
' '
|
114
|
+
when 1
|
115
|
+
player1.marker
|
116
|
+
when 2
|
117
|
+
player2.marker
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|