A month ago I posted an earlier version of the game here, and got a great response, which was mainly about the structure of my code. I freed some time today and reworked the code from scratch.Things I improved:Class separationNo more god objectApplied single responsibility principle for classes and methodsI would love to hear if the code can be improved even further (structure, logic, etc.), as this is I feel the maximum of my coding capability:# players in the gameclass Player  attr_reader :name, :symbol  def initialize(name, symbol)    @name = name    @symbol = symbol  endend# the game boardclass Board  attr_accessor :board  def initialize    @board = (1..9).to_a  end  def display_board    puts \e[H\e[2J # ANSI clear    @board.each_slice(3).with_index do |row, idx|      print   #{row.join(' | ')}\n      puts ' ---+---+---' unless idx == 2    end    puts  end  def welcome_msg    print \nWelcome to Tic Tac Toe.\n\n    puts 'Enter 1 to play against another player, 2 to play against an evil AI'\         ', 3 to watch evil AI play against kind AI.'    puts 'Type EXIT anytime to quit.'  end  def cell_open?(position)    @board[position - 1].is_a?(Fixnum) ? true : false  end  def win_game?(symbol)    sequences = [[0, 1, 2], [3, 4, 5], [6, 7, 8],                 [0, 3, 6], [1, 4, 7], [2, 5, 8],                 [0, 4, 8], [2, 4, 6]]    sequences.each do |seq|      return true if seq.all? { |a| @board[a] == symbol }    end    false  end  def full?    @board.each do |cell|      return false if cell.is_a? Fixnum    end    true  end  def place_mark(position, symbol)    @board[position - 1] = symbol  endend# game logicclass Game  attr_accessor :board, :player1, :player2, :ai, :current_player  def initialize    @board = Board.new    @player1 = Player.new('Player 1', 'X')    @player2 = Player.new('Player 2', 'O')    @ai = AI.new('Evil AI', 'O')    @ai2 = AI.new('Kind AI', 'X')    @current_player = @player1    start_screen  end  def start_screen    @board.welcome_msg    choice = nil    until (1..3).include?(choice)      choice = gets.chomp      exit if choice.downcase == 'exit'      game_modes(choice.to_i)    end  end  def game_modes(choice)    case choice    when 1 then pvp_game    when 2 then pva_game    when 3 then ava_game    else        puts 'You silly goose, try again.'    end  end  def pvp_game    @board.display_board    until game_over      player_place_n_check      swap_players    end  end  def pva_game    @board.display_board    until game_over      player_place_n_check      swap_pva      ai_place_n_check(@ai)      swap_pva    end  end  def ava_game    @board.display_board    until game_over      ai_place_n_check(@ai2)      swap_ava      ai_place_n_check(@ai)      swap_ava    end  end  def game_over    @board.win_game?(@current_player.symbol) || @board.full?  end  def player_place_n_check    position = player_input    @board.place_mark(position.to_i, @current_player.symbol)    @board.display_board    result?  end  def ai_place_n_check(player)    position = player.ai_turn(@board)    @board.place_mark(position.to_i, @current_player.symbol) unless position.nil?    @board.display_board    result?  end  def player_input    input = nil    until (1..9).include?(input) && @board.cell_open?(input)      puts Choose a number (1-9) to place your mark #{@current_player.name}.      input = validate_input(gets.chomp)    end    input  end  def validate_input(input)    if input.to_i == 0      exit if input.downcase == 'exit'      puts 'You can\'t use a string, silly.'    else      position = validate_position(input.to_i)    end    position  end  def validate_position(position)    if !(1..9).include? position      puts 'This position does not exist, chief.'      puts 'Try again or type EXIT to, well, exit.'    elsif !@board.cell_open? position      puts 'Nice try but this cell is already taken.'      puts 'Try again or type EXIT to, well, exit.'    end    position  end  def result?    if @board.win_game?(@current_player.symbol)      puts Game Over, #{@current_player.name} has won.      exit    elsif @board.full?      puts 'Draw.'      exit    end  end  def swap_players    case @current_player    when @player1 then @current_player = @player2    else               @current_player = @player1    end  end  def swap_pva    case @current_player    when @player1 then @current_player = @ai    else               @current_player = @player1    end  end  def swap_ava    @current_player == @ai ? @current_player = @ai2 : @current_player = @ai  endend# AI player componentsclass AI  attr_accessor :board, :name, :symbol  def initialize(name, symbol)    @name = name    @symbol = symbol  end  def ai_turn(board)    loading_simulation    check_win(board)    return @finished if @finished    check_block(board)    return @finished if @finished    check_defaults(board)    return @finished if @finished  end  # first check if possible to win before human player.  def check_win(board)    @finished = false    1.upto(9) do |i|      origin = board.board[i - 1]      board.board[i - 1] = 'O' if origin.is_a? Fixnum      # put it there if AI can win that way.      return @finished = i if board.win_game?('O')      board.board[i - 1] = origin    end  end  # if impossible to win before player,  # check if possible to block player from winning.  def check_block(board)    @finished = false    1.upto(9) do |i|      origin = board.board[i - 1]      board.board[i - 1] = 'X' if origin.is_a? Fixnum      # put it there if player can win that way.      return @finished = i if board.win_game?('X')      board.board[i - 1] = origin    end  end  # if impossible to win nor block, default placement to center.  # if occupied, choose randomly between corners or sides.  def check_defaults(board)    @finished = false    if board.board[4].is_a? Fixnum      @finished = 5    else      rand < 0.51 ? possible_sides(board) : possible_corners(board)    end  end  def possible_sides(board)    [2, 4, 6, 8].each do |i|      return @finished = i if board.board[i - 1].is_a? Fixnum    end  end  def possible_corners(board)    [1, 3, 7, 9].each do |i|      return @finished = i if board.board[i - 1].is_a? Fixnum    end  end  def loading_simulation    str = \r#{name} is scheming    10.times do      print str += '.'      sleep(0.1)    end  endendGame.new
TicTacToe game with functional AI in ruby - follow-up
ruby;tic tac toe;ai
EncapsulationInside the Game class:attr_accessor :board, :player1, :player2, :ai, :current_playerDon't expose these attributes as they are only used inside the class. (In ruby you don't have to declare instance variables / fields to use them)Also, make all internal methods private so they are not accessible from outside the class.Single Responsibility PrincipleGame should only be responsible for the logic of the game (having the players take turns). It should not be responsible for getting user input, so all user input related methods should be moved into Player (which I renamed to Human so it's obvious it only represents human players):class Human  attr_reader :name, :symbol  def initialize(board, name, symbol)    @board = board    @name = name    @symbol = symbol  end  def get_input      ...      puts Choose a number (1-9) to place your mark #{@name}. # name is available directly      ... end  private    def validate_input(input); ...; end    def validate_position(position); ...; endendNote I added board to Human so it can validate the input. I renamed player_input to get_input (it's obvious it's the player's input now becuase it's inside Human). Now Game can use Human like this:@player1 = Human.new(@board, 'Player 1', 'X')position = @current_player.get_inputPolymorphismA lot of code in Game is duplicated so it can handle both Human and AI. In order to avoid this duplication, we must be able to treat Humans and AIs the same way in the code. We can acheive this by making both classes expose the same interface.The interface Human exposes is a single method called get_input which takes no arguments and returns a position. We can make AI expose this same interface by renaming ai_turn to get_input, and moving the board argument to the constructor (like we did in Human):class AI  attr_reader :name, :symbol  def initialize(board, name, symbol)    @board = board    @name = name    @symbol = symbol  end  def get_input # no arguments      ...  end  private    # no need to pass board around, simply use @board    def check_win; ...; end    def check_block; ...; end    def check_defaults; ...; end;    def possible_sides; ...; end;    def possible_corners; ...; end;    def loading_simulation; ...; end;endNow, if we have a variable called player, we can call player.get_input and it will work both in the case player is a Human and in the case it's an AI. We can treat both cases the same way.This means Game can simply have 2 players, and the same logic will work for PvP, PvAI and AIvAI:class Game  def initialize    @board = Board.new    start_screen  end  private    def start_screen      @board.welcome_msg      choice = nil      until (1..3).include?(choice)        choice = gets.chomp        exit if choice.downcase == 'exit'        game_modes(choice.to_i)      end    end    def game_modes(choice)      case choice      when 1 then        @player1 = Human.new(@board, 'Player 1', 'X')        @player2 = Human.new(@board, 'Player 2', 'O')      when 2 then        @player1 = Human.new(@board, 'Player 1', 'X')        @player2 = AI.new(@board, 'Evil AI', 'O')      when 3 then        @player1 = AI.new(@board, 'Evil AI', 'O')        @player2 = AI.new(@board, 'Kind AI', 'X')      else puts 'You silly goose, try again.'      end      @current_player = @player1      run_game    end    def run_game      @board.display_board      until game_over        player_place_n_check        swap_players      end    end    def game_over; ...; end    def player_place_n_check      position = @current_player.get_input      @board.place_mark(position.to_i, @current_player.symbol) unless position.nil?      @board.display_board      result?    end    def result?; ...; end    def swap_players; ...; endendSingle Responsibility Principle (2)Game still does too much. It handles the main menu which is not related to the game logic. It's best to move the code that shows the menu and constructs Humans/AIs outside the Game class.This requires moving the board argument from the contructors of Human and AI to the get_input methods in both classes (sorry, didn't forsee it coming). Then make Game take the players as arguments so it can be instantiated like this (for example):Game.new(Human.new('Player 1', 'X'), AI.new('Evil AI', 'O'))Hopefully this fixes the structural problems with the code. After you follow these suggestions, you want to post a follow up question so you can get review for individual classes and methods.Good luck, and keep learning!