Last active
September 28, 2023 21:04
-
-
Save Nmerey/4df4bebeccc2bb85a5be954119dba950 to your computer and use it in GitHub Desktop.
Tick Tack Toe Game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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