ruby_ttt 0.0.7 → 0.0.8
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/lib/ai.rb +66 -0
- data/lib/board.rb +145 -0
- data/lib/game.rb +59 -0
- data/lib/game_setup.rb +76 -0
- data/lib/player.rb +63 -0
- data/lib/ui.rb +118 -0
- metadata +9 -3
data/lib/ai.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
POS_INF = 999
|
2
|
+
NEG_INF = -999
|
3
|
+
WIN = 1
|
4
|
+
LOSE = -1
|
5
|
+
TIE = 0
|
6
|
+
|
7
|
+
class AI
|
8
|
+
|
9
|
+
def computer_move(board, player)
|
10
|
+
test_board = board.dup
|
11
|
+
test_board.all_cells = board.all_cells.dup
|
12
|
+
get_best_move(test_board, player)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_best_move(board, player)
|
16
|
+
ranked_moves = rank_possible_moves(board, player)
|
17
|
+
move = ranked_moves.max_by {|cell, score| score}
|
18
|
+
move.first
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def rank_possible_moves(board, player)
|
24
|
+
possible_moves = board.open_cells
|
25
|
+
possible_moves.each_key do |cell|
|
26
|
+
possible_moves[cell] = get_move_score(board, player, cell)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_move_score(board, player, cell)
|
31
|
+
board.add_marker(player.marker, cell)
|
32
|
+
best_score = apply_minimax(board, player, cell, depth=0, NEG_INF, POS_INF)
|
33
|
+
board.remove_marker(cell)
|
34
|
+
best_score
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_score(board, player)
|
38
|
+
return WIN if board.winner?(player.marker) && player.current_player?
|
39
|
+
return LOSE if board.winner?(player.marker)
|
40
|
+
TIE
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_minimax(board, player, cell, depth, alpha, beta)
|
44
|
+
return get_score(board, player) if board.game_over?
|
45
|
+
if player.current_player?
|
46
|
+
maximizing_player = Maximizing.new(player)
|
47
|
+
alphabeta(board, maximizing_player, depth, alpha, beta)
|
48
|
+
else
|
49
|
+
minimizing_player = Minimizing.new(player)
|
50
|
+
alphabeta(board, minimizing_player, depth, alpha, beta)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def alphabeta(board, player, depth, alpha, beta)
|
55
|
+
board.open_cells.each_key do |cell|
|
56
|
+
board.add_marker(player.opponent.marker, cell)
|
57
|
+
score = (apply_minimax(board, player.opponent, cell, depth += 1, alpha, beta) / depth.to_f)
|
58
|
+
alpha = player.get_alpha(alpha, score)
|
59
|
+
beta = player.get_beta(beta, score)
|
60
|
+
board.remove_marker(cell)
|
61
|
+
break if alpha >= beta
|
62
|
+
end
|
63
|
+
player.return_value(alpha, beta)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/board.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
MARKER_X = 'X'
|
2
|
+
MARKER_O = 'O'
|
3
|
+
class Board
|
4
|
+
attr_accessor :all_cells, :num_of_rows, :winning_lines
|
5
|
+
def initialize(num_of_rows)
|
6
|
+
@num_of_rows = num_of_rows
|
7
|
+
@all_cells = create_board_hash
|
8
|
+
@winning_lines = get_winning_lines
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_board_hash
|
12
|
+
new_board = Hash.new
|
13
|
+
alpha = 'A'
|
14
|
+
numeric = 1
|
15
|
+
num_of_rows.times do
|
16
|
+
num_of_rows.times do
|
17
|
+
cellID = numeric.to_s + alpha
|
18
|
+
numeric += 1
|
19
|
+
new_board[cellID] = nil
|
20
|
+
end
|
21
|
+
alpha = alpha.next
|
22
|
+
numeric = 1
|
23
|
+
end
|
24
|
+
new_board
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_winning_lines
|
28
|
+
lines = []
|
29
|
+
all_rows.each { |row| lines << row }
|
30
|
+
all_cols.each { |col| lines << col }
|
31
|
+
diagonals.each { |diagonal| lines << diagonal }
|
32
|
+
lines
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_rows
|
36
|
+
rows = []
|
37
|
+
cellIDs = all_cells.keys
|
38
|
+
beg = 0
|
39
|
+
ending = num_of_rows - 1
|
40
|
+
until rows.length == num_of_rows
|
41
|
+
rows << cellIDs[beg..ending]
|
42
|
+
beg += num_of_rows
|
43
|
+
ending += num_of_rows
|
44
|
+
end
|
45
|
+
rows
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_marker(marker, cell)
|
49
|
+
all_cells[cell] = marker
|
50
|
+
end
|
51
|
+
|
52
|
+
def winner?(marker)
|
53
|
+
board_markers = all_cells.select { |k,v| v == marker }.keys
|
54
|
+
winning_lines.each do |line|
|
55
|
+
return true if (line & board_markers).length == num_of_rows
|
56
|
+
end
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def game_over?
|
61
|
+
!moves_remaining? || winner?(MARKER_X)|| winner?(MARKER_O)
|
62
|
+
end
|
63
|
+
|
64
|
+
def available_cell?(cell)
|
65
|
+
valid_cell?(cell) && all_cells[cell].nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid_cell?(cell)
|
69
|
+
all_cells.has_key?(cell)
|
70
|
+
end
|
71
|
+
|
72
|
+
def remove_marker(cell)
|
73
|
+
all_cells[cell] = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def moves_remaining?
|
77
|
+
all_cells.has_value?(nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
def open_cells
|
81
|
+
all_cells.select { |k,v| v.nil? }
|
82
|
+
end
|
83
|
+
|
84
|
+
def empty?
|
85
|
+
open_cells.length == (num_of_rows * num_of_rows)
|
86
|
+
end
|
87
|
+
|
88
|
+
def random_cell
|
89
|
+
cells = open_cells.keys
|
90
|
+
cells_count = cells.length - 1
|
91
|
+
cells[rand(cells_count)]
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def all_cols
|
96
|
+
cols = []
|
97
|
+
index = 0
|
98
|
+
num_of_rows.times do
|
99
|
+
cols << get_column(index)
|
100
|
+
index += 1
|
101
|
+
end
|
102
|
+
cols
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_column(index)
|
106
|
+
column = []
|
107
|
+
cellIDs = all_cells.keys
|
108
|
+
num_of_rows.times do
|
109
|
+
column << cellIDs[index]
|
110
|
+
index += num_of_rows
|
111
|
+
end
|
112
|
+
column
|
113
|
+
end
|
114
|
+
|
115
|
+
def diagonals
|
116
|
+
diagonals = []
|
117
|
+
diagonals << diagonal_one
|
118
|
+
diagonals << diagonal_two
|
119
|
+
end
|
120
|
+
|
121
|
+
def diagonal_one
|
122
|
+
diagonal = []
|
123
|
+
alpha = 'A'
|
124
|
+
numeric = 1
|
125
|
+
num_of_rows.times do
|
126
|
+
diagonal << numeric.to_s + alpha
|
127
|
+
alpha = alpha.next
|
128
|
+
numeric += 1
|
129
|
+
end
|
130
|
+
diagonal
|
131
|
+
end
|
132
|
+
|
133
|
+
def diagonal_two
|
134
|
+
diagonal = []
|
135
|
+
alpha = 'A'
|
136
|
+
numeric = num_of_rows
|
137
|
+
num_of_rows.times do
|
138
|
+
diagonal << numeric.to_s + alpha
|
139
|
+
alpha = alpha.next
|
140
|
+
numeric -= 1
|
141
|
+
end
|
142
|
+
diagonal
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
data/lib/game.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'ai'
|
2
|
+
require 'board'
|
3
|
+
require 'player'
|
4
|
+
require 'ui'
|
5
|
+
require 'game_setup'
|
6
|
+
class Game
|
7
|
+
attr_accessor :board, :ui, :player_one, :player_two, :ai, :difficulty_level
|
8
|
+
def initialize(board, player_one, player_two, difficulty_level)
|
9
|
+
@board = board
|
10
|
+
@ui = UI.new(@board)
|
11
|
+
@player_one = player_one
|
12
|
+
@player_two = player_two
|
13
|
+
@ai = AI.new
|
14
|
+
@difficulty_level = difficulty_level
|
15
|
+
end
|
16
|
+
|
17
|
+
def play!
|
18
|
+
until board.game_over?
|
19
|
+
ui.display_board
|
20
|
+
move = get_next_move
|
21
|
+
board.available_cell?(move) ? advance_game(move, current_player) : invalid_move(move)
|
22
|
+
end
|
23
|
+
exit_game
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_next_move
|
27
|
+
return ui.request_human_move if current_player.player_type == HUMAN_PLAYER
|
28
|
+
difficulty_level == HARD_LEVEL ? ai.computer_move(board, current_player) : board.random_cell
|
29
|
+
end
|
30
|
+
|
31
|
+
def advance_game(cell, player)
|
32
|
+
board.add_marker(player.marker, cell)
|
33
|
+
game_status_check
|
34
|
+
player.next_player_turn
|
35
|
+
ui.next_move_message(current_player) unless board.game_over?
|
36
|
+
end
|
37
|
+
|
38
|
+
def game_status_check
|
39
|
+
if board.winner?(current_player.marker)
|
40
|
+
ui.winning_game_message(current_player)
|
41
|
+
elsif !board.moves_remaining?
|
42
|
+
ui.tie_game_message
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def invalid_move(cell)
|
47
|
+
board.valid_cell?(cell) ? ui.taken_cell_message(cell) : ui.bad_cell_message(cell)
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_player
|
51
|
+
player_one.current_player? ? player_one : player_two
|
52
|
+
end
|
53
|
+
|
54
|
+
def exit_game
|
55
|
+
ui.display_board
|
56
|
+
ui.io.exit
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/lib/game_setup.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
EASY_LEVEL = 'easy'
|
2
|
+
HARD_LEVEL = 'hard'
|
3
|
+
COMPUTER_PLAYER = 'computer'
|
4
|
+
HUMAN_PLAYER = 'human'
|
5
|
+
class GameSetup
|
6
|
+
attr_accessor :ui, :board, :player_one, :player_two
|
7
|
+
def initialize(board, player_one, player_two)
|
8
|
+
@board = board
|
9
|
+
@ui = UI.new(@board)
|
10
|
+
@player_one = player_one
|
11
|
+
@player_two = player_two
|
12
|
+
end
|
13
|
+
|
14
|
+
def start!
|
15
|
+
begin
|
16
|
+
set_opponents
|
17
|
+
get_player_type(player_one)
|
18
|
+
get_player_type(player_two)
|
19
|
+
level = get_difficulty_level
|
20
|
+
who_goes_first
|
21
|
+
Game.new(board, player_one, player_two, level).play!
|
22
|
+
rescue Interrupt
|
23
|
+
ui.early_exit_message
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_opponents
|
29
|
+
player_one.opponent = player_two
|
30
|
+
player_two.opponent = player_one
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_player_type(player)
|
34
|
+
type = ui.request_player_type(player.marker)
|
35
|
+
validate_type(type, player) ? set_player_type(type, player) : invalid_type(type, player)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_difficulty_level
|
39
|
+
return nil unless player_one.player_type == COMPUTER_PLAYER || player_two.player_type == COMPUTER_PLAYER
|
40
|
+
level = ui.request_difficulty_level
|
41
|
+
validate_level(level) ? ui.level_assigned_message(level) : invalid_level(level)
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_type(type, player)
|
45
|
+
(type == HUMAN_PLAYER) || (type == COMPUTER_PLAYER)
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_player_type(type, player)
|
49
|
+
player.player_type = type
|
50
|
+
ui.type_assigned_message(type, player.marker)
|
51
|
+
end
|
52
|
+
|
53
|
+
def invalid_type(type, player)
|
54
|
+
ui.invalid_input_message(type)
|
55
|
+
get_player_type(player)
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_level(level)
|
59
|
+
(level == HARD_LEVEL) || (level == EASY_LEVEL)
|
60
|
+
end
|
61
|
+
|
62
|
+
def invalid_level(level)
|
63
|
+
ui.invalid_input_message(level)
|
64
|
+
get_difficulty_level
|
65
|
+
end
|
66
|
+
|
67
|
+
def who_goes_first
|
68
|
+
rand(0..1) == 1 ? set_first_turn(player_one) : set_first_turn(player_two)
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_first_turn(player)
|
72
|
+
player.turn = 1
|
73
|
+
ui.first_move_message(player)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/player.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
class Player
|
2
|
+
attr_accessor :marker, :turn, :player_type, :opponent
|
3
|
+
def initialize(marker)
|
4
|
+
@marker = marker
|
5
|
+
@player_type = 'human'
|
6
|
+
@turn = 0
|
7
|
+
@opponent = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def next_player_turn
|
11
|
+
self.turn = 0
|
12
|
+
self.opponent.turn = 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_player?
|
16
|
+
self.turn == 1
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_alpha(alpha, score)
|
20
|
+
alpha
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_beta(beta, score)
|
24
|
+
beta
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Minimizing < Player
|
29
|
+
attr_accessor :marker, :turn, :opponent
|
30
|
+
def initialize(player)
|
31
|
+
@marker = player.marker
|
32
|
+
@turn = player.turn
|
33
|
+
@opponent = player.opponent
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_alpha(alpha, score)
|
37
|
+
score > alpha ? score : alpha
|
38
|
+
end
|
39
|
+
|
40
|
+
def return_value(alpha, beta)
|
41
|
+
alpha
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Maximizing < Player
|
47
|
+
attr_accessor :marker, :turn, :opponent
|
48
|
+
def initialize(player)
|
49
|
+
@marker = player.marker
|
50
|
+
@turn = player.turn
|
51
|
+
@opponent = player.opponent
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_beta(beta, score)
|
55
|
+
score < beta ? score : beta
|
56
|
+
end
|
57
|
+
|
58
|
+
def return_value(alpha, beta)
|
59
|
+
beta
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
data/lib/ui.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
class UI
|
2
|
+
attr_accessor :board, :io
|
3
|
+
def initialize(board)
|
4
|
+
@board = board
|
5
|
+
@io = Kernel
|
6
|
+
end
|
7
|
+
|
8
|
+
def request_player_type(marker)
|
9
|
+
player_type_message(marker)
|
10
|
+
io.gets.chomp.downcase
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_difficulty_level
|
14
|
+
difficulty_level_message
|
15
|
+
io.gets.chomp.downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_human_move
|
19
|
+
standardize(io.gets.chomp)
|
20
|
+
end
|
21
|
+
|
22
|
+
def standardize(input)
|
23
|
+
input.split('').sort.join('').upcase
|
24
|
+
end
|
25
|
+
|
26
|
+
def difficulty_level_message
|
27
|
+
io.print "Select computer difficulty level: Enter 'easy' or 'hard.'\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def level_assigned_message(level)
|
31
|
+
io.print "You selected difficulty level #{level.upcase}.\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
def invalid_input_message(input)
|
35
|
+
io.print " #{input} is not a valid option.\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def player_type_message(marker)
|
39
|
+
io.print "For player " + "'#{marker}'," + " enter 'human' or 'computer.'\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def type_assigned_message(type, marker)
|
43
|
+
io.print "Player " + "'#{marker}' " + "is #{type}.\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_board_numbers
|
47
|
+
num = 1
|
48
|
+
io.print " "
|
49
|
+
board.num_of_rows.times do
|
50
|
+
io.print "--#{num}-- "
|
51
|
+
num += 1
|
52
|
+
end
|
53
|
+
io.print "\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
def print_divider
|
57
|
+
io.print " "
|
58
|
+
board.num_of_rows.times { io.print "------" }
|
59
|
+
io.print "\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_board_rows
|
63
|
+
alpha = 'A'
|
64
|
+
board.all_rows.each do |row|
|
65
|
+
show_row(alpha, row)
|
66
|
+
alpha = alpha.next
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def show_row(letter, cells)
|
71
|
+
io.print "#{letter}"
|
72
|
+
cells.each { |cell| io.print " | " + show_marker(cell) }
|
73
|
+
io.print " | #{letter}\n"
|
74
|
+
print_divider
|
75
|
+
end
|
76
|
+
|
77
|
+
def show_marker(cell)
|
78
|
+
board.all_cells[cell].nil? ? ' ' : board.all_cells[cell]
|
79
|
+
end
|
80
|
+
|
81
|
+
def display_board
|
82
|
+
print_board_numbers
|
83
|
+
print_board_rows
|
84
|
+
print_board_numbers
|
85
|
+
end
|
86
|
+
|
87
|
+
def first_move_message(player)
|
88
|
+
io.print "\n\n************ New Game ************\n"
|
89
|
+
io.print "Player '#{player.marker}' goes first.\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_move_message(player)
|
93
|
+
io.print "Player '#{player.marker}': Enter open cell ID.\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
def winning_game_message(player)
|
97
|
+
io.print "GAME OVER! Player '#{player.marker}' wins!\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
def tie_game_message
|
101
|
+
io.print "GAME OVER! It's a tie!\n"
|
102
|
+
end
|
103
|
+
|
104
|
+
def taken_cell_message(cell)
|
105
|
+
io.print "#{cell} has already been taken!\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
def bad_cell_message(cell)
|
109
|
+
io.print "#{cell} is not a valid cell ID!\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
def early_exit_message
|
113
|
+
io.print "\nExiting Tic-Tac-Toe..."
|
114
|
+
io.print "...\n"
|
115
|
+
io.print "Goodbye!\n\n"
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_ttt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -34,13 +34,19 @@ email: tsauer@8thlight.com
|
|
34
34
|
executables: []
|
35
35
|
extensions: []
|
36
36
|
extra_rdoc_files: []
|
37
|
-
files:
|
37
|
+
files:
|
38
|
+
- lib/ai.rb
|
39
|
+
- lib/board.rb
|
40
|
+
- lib/game.rb
|
41
|
+
- lib/game_setup.rb
|
42
|
+
- lib/player.rb
|
43
|
+
- lib/ui.rb
|
38
44
|
homepage: http://rubygems.org/gems/ruby_ttt
|
39
45
|
licenses: []
|
40
46
|
post_install_message:
|
41
47
|
rdoc_options: []
|
42
48
|
require_paths:
|
43
|
-
-
|
49
|
+
- lib
|
44
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
45
51
|
none: false
|
46
52
|
requirements:
|