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