bin/xo
#!/usr/bin/env ruby
#
# A Tic-tac-toe game client based on xo.
require 'xo'
include XO
AUTHOR = "Dwayne Crooks"
EMAIL = "me@dwaynecrooks.com"
class Player
def moves(grid, turn)
raise NotImplementedError
end
private
def all_open_moves(grid)
moves = []
grid.each_open do |r, c|
moves << [r, c]
end
moves
end
def all_smart_moves(grid, turn)
AI::Minimax.instance.moves(grid, turn)
end
end
class Easy < Player
def moves(grid, turn)
all_open_moves(grid)
end
end
class Medium < Player
def moves(grid, turn)
smart_moves = all_smart_moves(grid, turn)
dumb_moves = all_open_moves(grid) - smart_moves
dumb_moves.empty? ? smart_moves : (rand < 0.75 ? smart_moves : dumb_moves)
end
end
class Hard < Player
def moves(grid, turn)
all_smart_moves(grid, turn)
end
end
class Human
def initialize(humans)
@humans = humans
end
def get_move(grid, turn)
puts grid
puts
puts "Enter your move in the format r, c."
prompt_for_move(grid, turn)
end
def handle_next_turn(grid)
puts grid
puts
end
def handle_game_over(grid, event)
case event[:type]
when :winner
puts "Congratulations! #{@humans == 2 ? event[:last_move][:turn] : 'You'} won."
when :squashed
puts "Sorry! Game squashed."
end
puts
end
def handle_invalid_move(event)
puts "Sorry! You cannot make that move."
case event[:type]
when :occupied
puts "That position is occupied."
when :out_of_bounds
puts "That position is not on the board."
end
puts
end
private
def prompt_for_move(grid, turn)
print "> "
position = gets.chomp.split(',').map(&:strip).map(&:to_i)
if position.length == 2
puts
position
else
prompt_for_move(grid, turn)
end
end
end
class Computer
def initialize(humans, ai)
@humans = humans
@ai = ai
end
def get_move(grid, turn)
puts "Waiting for the computer to play..." if @humans == 1
r, c = @ai.moves(grid, turn).shuffle[0]
if @humans == 1
puts "The computer played at #{r}, #{c}."
puts
end
[r, c]
end
def handle_next_turn(grid)
end
def handle_game_over(grid, event)
if @humans == 0
case event[:type]
when :winner
puts "#{event[:last_move][:turn]} wins!"
when :squashed
puts "Game squashed!"
end
elsif @humans == 1
puts grid
puts
case event[:type]
when :winner
puts "Sorry! You lost."
when :squashed
puts "Sorry! Game squashed."
end
puts
end
end
def handle_invalid_move(event)
end
end
class Game
def initialize(players)
@players = players
@engine = Engine.new
@engine.add_observer(self)
end
def start(turn = false)
turn ? engine.start(turn) : engine.continue_playing
end
def run
run_one_turn until engine.current_state == GameOver
end
def update(event)
player = players[engine.turn]
case event[:name]
when :next_turn
player.handle_next_turn(engine.grid)
when :game_over
player.handle_game_over(engine.grid, event)
when :invalid_move
player.handle_invalid_move(event)
end
end
private
attr_reader :players, :engine
def run_one_turn
turn = engine.turn
r, c = players[turn].get_move(engine.grid, turn)
engine.play(r, c)
end
end
def welcome
heading = "Welcome to xo! A Tic-tac-toe game client created by #{AUTHOR}."
puts heading
puts "=" * heading.length
puts
puts "NOTE: You can exit the game at anytime by pressing CTRL-C."
puts
end
def ask_for_difficulty_level
puts "Please select your difficulty level:"
puts "1. Easy"
puts "2. Medium"
puts "3. Hard"
get_option({ '1' => Easy, '2' => Medium, '3' => Hard })
end
def ask_for_token
puts "Do you want to play as x or o?"
get_option({ 'x' => Grid::X, 'o' => Grid::O })
end
def ask_to_play_first
puts "Do you want to play first (y or n)?"
get_option({ 'y' => true, 'n' => false })
end
def ask_to_play_again
puts "Do you want to play again (y or n)?"
get_option({ 'y' => true, 'n' => false })
end
def get_option(options)
print "> "
selection = gets.chomp
if options.key?(selection)
puts
options[selection]
else
get_option(options)
end
end
def say_goodbye
puts "Thank you for playing."
puts "Bye!"
end
def terminate
puts
puts
say_goodbye
exit
end
def set_up_ctrl_c_handler
trap("INT") {
terminate
}
end
def main
set_up_ctrl_c_handler
welcome
level = ask_for_difficulty_level
human_token = ask_for_token
computer_token = Grid.other_token(human_token)
play_first = ask_to_play_first
players = {}
players[human_token] = Human.new(1)
players[computer_token] = Computer.new(1, level.new)
game = Game.new(players)
game.start(play_first ? human_token : computer_token)
loop do
game.run
play_again = ask_to_play_again
break unless play_again
game.start
end
say_goodbye
end
main