Skip to content

Instantly share code, notes, and snippets.

@Nmerey
Last active September 28, 2023 21:04
Show Gist options
  • Save Nmerey/4df4bebeccc2bb85a5be954119dba950 to your computer and use it in GitHub Desktop.
Save Nmerey/4df4bebeccc2bb85a5be954119dba950 to your computer and use it in GitHub Desktop.
Tick Tack Toe Game
module Chatbot
def ask_game_options
info = {}
info[:player_numbers] = ask_number_of_players
info[:hardness_level] = ask_hardness_level if info[:player_numbers] == 1
info
end
def get_spot
ask_for_spot(self)
end
def spot_invalid_message
puts "Spot is invalid please choose another one!"
end
def final_words
puts "Wanna hear a joke? (yes/no)"
answer = gets.chomp.to_s
answer == "yes" ? tell_joke : say_goodbye
end
def game_over_message
puts "GAME OVER"
end
private
def ask_for_spot(player)
puts "#{player.sign}'s turn. Please choose spot(1..9)"
spot = gets.chomp.to_i
unless (1..9).include?(spot)
invalid_entry_message
spot = ask_for_spot(player)
end
spot
end
def invalid_entry_message
puts "Please enter correct input\n"
end
def ask_number_of_players
puts "How many players are going to play(0..2)"
players_count = gets.chomp.to_i
until (1..2).include?(players_count)
invalid_entry_message
players_count = ask_number_of_players
end
players_count
end
def ask_hardness_level
puts "Please choose level of hardness. \n1 - Easy \n2 - Medium \n3 - Hard"
hardness_level = gets.chomp.to_i
unless (1..3).include?(hardness_level)
invalid_entry_message
hardness_level = ask_hardness_level
end
hardness_level
end
def tell_joke
puts "\nKnock, Knock who is there? \nStatue \nStatue who? \nSta-tue bro!"
end
def say_goodbye
puts "Peace to you! Goodbye"
end
def tie_message
puts "Friendship won. Game is TIE"
end
end
class GameStarter
include Chatbot
def initialize
end
def start
game_options = ask_game_options
start_game_with(game_options)
shutdown_gracefully
end
private
def start_game_with(options)
Game.new(options).start
end
def shutdown_gracefully
final_words
end
end
class Game
include Chatbot
PLAYER_TYPES = {
2 => ["human", "human"],
1 => ["human", "robot"],
0 => ["robot", "robot"]
}
attr_reader :hardness_level, :player_numbers, :board
def initialize(args)
@player_numbers = args[:player_numbers] || 1
@hardness_level = args[:hardness_level] || 3
@board = Board.new
end
def start
set_players
board.draw
until board.game_over? || board.tie_game?
@first_player.move_on(board, hardness_level)
return tie_message if board.tie_game?
return game_over_message if board.game_over?
@second_player.move_on(board, hardness_level)
end
game_over_message
end
private
def set_players
PLAYER_TYPES[player_numbers].each_with_index { |type, indeks| create_robot_or_human(type, indeks) }
end
def create_robot_or_human(type, number)
type == "human" ? create_human_player(number) : create_robot_player(number)
end
def create_human_player(number)
var_num = number == 0 ? "first" : "second"
instance_variable_set("@#{var_num}_player", Player.new(number))
end
def create_robot_player(number)
var_num = number == 0 ? "first" : "second"
instance_variable_set("@#{var_num}_player", Robot.new(number))
end
end
class Player
include Chatbot
attr_reader :number, :sign
def initialize(number)
@number = number
end
def sign
number == 0 ? "X" : "O"
end
def move_on(board, hardness_level)
spot = choose_spot(board)
board.fill_spot(spot,sign)
board.draw
end
private
def choose_spot(board)
spot = get_spot
until board.spot_valid?(spot)
spot_invalid_message
spot = get_spot
end
spot
end
end
class Robot < Player
def move_on(board, hardness_level = 3)
lvl_rated_moves = analyze_position_on(board, sign)
spot = lvl_rated_moves[hardness_level - 1]
puts "SPOT: #{spot}"
board.fill_spot(spot,sign)
board.draw
end
private
def analyze_position_on(board, sign)
move_scores = Hash.new { |hash,key| hash[key] = [] }
possible_moves = board.state.flatten.select {|spot| spot.is_a?(Integer)}
# Sort moves by their rating
possible_moves.each do |move|
move_rating = rate_move(move, board, sign)
move_scores[move_rating] << move
end
# Take possible moves for each hardness level
lvl_rated_moves = []
sorted_keys = move_scores.keys.sort
sorted_keys.each do |key|
lvl_rated_moves << move_scores[key].sample
end
until lvl_rated_moves.count == 3
lvl_rated_moves << move_scores[sorted_keys.last].sample
end
# Edge case when hardest level and board is empty or only one move played
# 5 is the best play for Hardest Level
lvl_rated_moves[2] = 5 if edge_case?(move_scores, possible_moves)
lvl_rated_moves
end
def edge_case?(move_scores, possible_moves)
move_scores.count == 1 && move_scores.keys.first == 33 && possible_moves.include?(5)
end
def rate_move(move, board, sign)
opponent_sign = sign == "X" ? "O" : "X"
temp_board = board.dup
# Try to block opponents winning move
temp_board.fill_spot(move, opponent_sign)
game_over = temp_board.game_over?
temp_board.fill_spot(move,move)
return 99 if game_over
# Try to find winning move
temp_board.fill_spot(move, sign)
game_over = temp_board.game_over?
temp_board.fill_spot(move, move)
return 100 if game_over
# Find best score after winning ones
row_win_comb = temp_board.state
col_win_comb = temp_board.state.transpose
state = temp_board.state
diag_win_comb = [[state[0][0],state[1][1],state[2][2]],[state[0][2],state[1][1],state[2][0]]]
win_condition = [row_win_comb, col_win_comb, diag_win_comb]
# Find best combinations with 2 slots left
sorted_ratings = Hash.new { |hash, key| hash[key] = [] }
win_condition.each do |comb|
int_count = comb.select { |spot| spot.is_a?(Integer)}.count
sorted_ratings[int_count] << comb
end
# Find combinations with same sign
best_combinations = sorted_ratings[2].select { |comb| comb.include?(sign) }
best_chance = best_combinations.empty? ? 33 : 66
best_chance
end
end
class Board
attr_accessor :state
def initialize
# Creates nested array with numbers 1..9
@state = Array.new(3) { |i| Array.new(3) { |k| i * 3 + (k + 1) } }
end
def draw
board = ""
line = "\n-------------\n"
board << line
state.each do |row|
row.each do |sign|
board << "| #{sign} "
end
board << "|" + line
end
puts board
end
def just_started?
state.flatten.all?{ |spot| spot.is_a?(Integer) }
end
def spot_valid?(spot)
state.flatten[spot - 1].is_a?(Integer)
end
def fill_spot(spot, sign)
# Calculate the row of spot e.g 7 => state[2]
row = (spot -1) / 3
# Calculate the column index of spot e.g 7 => state[2][0]
col = spot % 3 - 1
state[row][col] = sign
end
def game_over?
row_win = state
col_win = state.transpose
diag_win = [[state[0][0],state[1][1],state[2][2]],[state[0][2],state[1][1],state[2][0]]]
win_condition = row_win + col_win + diag_win
win_condition.each { |combination| return true if combination.uniq.length == 1 }
return false
end
def tie_game?
state.flatten.all? { |s| s == "X" || s == "O" }
end
end
GameStarter.new.start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment