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.
Files changed (7) hide show
  1. data/lib/ai.rb +66 -0
  2. data/lib/board.rb +145 -0
  3. data/lib/game.rb +59 -0
  4. data/lib/game_setup.rb +76 -0
  5. data/lib/player.rb +63 -0
  6. data/lib/ui.rb +118 -0
  7. 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.7
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: