首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Ruby中带有AI的简单TicTacToe游戏

Ruby中带有AI的简单TicTacToe游戏
EN

Code Review用户
提问于 2015-09-29 10:05:01
回答 2查看 1.4K关注 0票数 5

你建议从结构、逻辑等方面进行哪些改变?

代码语言:javascript
复制
# Checks if, for example, "2" is an integer "in disguise".
class String
  def is_integer?
    to_i.to_s == self
  end
end

# Main TicTacToe game class
class Game
  def initialize
    @board = (1..9).to_a
    @running = true
  end

  def display_board
    puts "\n -----------"
    @board.each_slice(3) do |row|
      print '  '
      puts row.join(' | ')
      puts ' -----------'
    end
    puts
  end

  def determine_player(player)
    if player == :X
      return :X.to_s
    elsif player == :O
      return :O.to_s
    end
  end

  def turn(chosen_player)
    display_board
    puts "Choose a number (1-9) to place your mark on, Player #{chosen_player}."
    position = gets.chomp

    # using personal created method to determine input
    position = position.to_i if position.is_integer?

    if @board.include?(position)
      @board.map! do |num|
        if num == position
          determine_player(chosen_player)
        else
          num
        end
      end
    elsif position.is_a?(String)
      if position.downcase == 'exit'
        puts 'Wow, rude. Bye.'
        exit
      end
      puts 'Position can only be a number, silly.'
      puts 'Try again or type EXIT to, well, exit.'
      turn(chosen_player)
    else
      puts 'This position does not exist or already occupied, chief.'
      puts 'Try again or type EXIT to, well, exit.'
      turn(chosen_player)
    end
  end

  def win_game?
    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]]
    b = @board

    sequences.each do |sequence|
      if sequence.all? { |a| b[a] == 'X' }
        return true
      elsif sequence.all? { |a| b[a] == 'O' }
        return true
      end
    end
    false
  end

  def draw?
    @board.all? { |all| all.is_a? String } # returns true if no one has won.
  end

  def result?
    if win_game?
      display_board
      puts 'Game Over'
      @running = false
    elsif draw?
      display_board
      puts 'Draw'
      @running = false
    end
  end

  def playergame_progress
    while @running
      turn(:X)
      result?
      break if !@running
      turn(:O)
      result?
    end
  end

  # AI components
  def try_sides
    [1, 3, 5, 7].each do |idx|
      return @board[idx] = 'O' if @board[idx].is_a? Fixnum
    end
  end

  def try_corners
    [0, 2, 6, 8].each do |idx|
      return @board[idx] = 'O' if @board[idx].is_a? Fixnum
    end
  end

  def ai_turn
    # first check if possible to win before human player.
    0.upto(8) do |i|
      origin = @board[i]
      @board[i] = 'O' if @board[i] != 'X'
      win_game? ? return : @board[i] = origin # return for early breakout if won game.
    end

    # if impossible to win before player, check if possible to block player from winning.
    0.upto(8) do |i|
      origin = @board[i]
      @board[i] = 'X' if @board[i] != 'O'
      if win_game?
        return @board[i] = 'O' # if player can win that way, place it there before him.
      else
        @board[i] = origin
      end
    end

    # if impossible to win nor block, default placement to center.
    # if occupied, choose randomly between corners or sides.
    if @board[4].is_a? Fixnum
      return @board[4] = 'O'
    else
      rand > 0.499 ? try_sides || try_corners : try_corners || try_sides
    end
  end

  def thinking_simulation
    str = "\rEvil AI is scheming"
    5.times do
      print str += '.'
      sleep(0.3)
    end
  end

  def aigame_progress
    if rand > 0.3
      while @running
        turn(:X)
        result?
        break if !@running
        thinking_simulation
        ai_turn
        result?
      end
    else
      while @running
        thinking_simulation
        ai_turn
        result?
        break if !@running
        turn(:X)
        result?
      end
    end
  end
end

def play
  match = Game.new

  puts 'Welcome to Tic Tac Toe'
  puts 'Enter 1 to play against another player, or 2 to play against an evil AI.'
  puts 'Type EXIT anytime to quit.'

  choice = gets.chomp.to_i
  case choice
  when 1 then match.playergame_progress
  when 2 then match.aigame_progress
  else        puts 'You silly, you.'
  end
end

play
EN

回答 2

Code Review用户

回答已采纳

发布于 2015-09-29 17:34:48

上面的代码是害怕增加类的例子。这有两个症状:Game是一个上帝对象,以及对原语的过度依附(或者它是Ruby,过度依赖于内置类)。有趣的是,即使在这样简单的例子中,我们也可以看到为什么这些都是坏事。

考虑这两种方法(我现在减少了一些不重要的代码):

代码语言:javascript
复制
def display_board
  @board.each_slice(3) do |row|
    puts row.join(' | ')
    puts " ---------"
  end
end

def draw?
  @board.all? { |all| all.is_a? String } # returns true if no one has won.
end

诚然,使用Fixnum和String作为@board的元素是明智的,它使您的代码简洁,但也很难读,事实证明,您需要注释来解释只有单行长的方法。除非有人(特别是:你,两个月后)彻底阅读你的代码,否则在一开始is_a? String意味着一个空字段(即使有这样的注释),这并不明显。类似地,row.join是否能够产生类似0 | X | 2的东西也不是很明显。考虑使用类而不是原语的例子。@board现在是字段数组):

代码语言:javascript
复制
class Field
  attr_accessor :owner

  def taken?
    owner.nil?
  end

  def empty?
    !taken?
  end
end

def draw?
  @board.all &:taken?
end

def display_board
  @board.each.with_index(1) |field, idx|
    print "#{field.owner || idx} | "
    print "\n-----------\n" if idx % 3 == 0
  end
end

它更冗长,也不理想,因为@owner仍然是一个符号,但是#draw?#display_board现在可以更好地揭示它们的工作方式(尽管#each_slice看起来确实比模块好,但读者现在不需要滚动代码来查找#display_board实际打印的内容。这将适用于代码的其他部分,即

代码语言:javascript
复制
if @board.include?(position)

代码语言:javascript
复制
if @board[position].empty?

在使用类时,我们可以描述正在做的事情,原语迫使我们描述事情是如何完成的。另外,可能更重要的好处是,即使我们改变了字段类的工作方式,像#draw?这样的方法也会同样工作--我们实现了封装封装

上帝的对象是众所周知的代码气味。想想看:如果您只有一个类,即只实例化一次(而且您需要实例化),那么您甚至不需要对象和类--只需将您的@s更改为$s,以便您的状态保持在全局范围内,而不是对象的状态(这并没有什么不同,因为您的所有代码都使用相同的状态!),您的代码将同样工作。您可能知道为什么globals是不明智的,正如您可以看到您的代码工作在全局,只是假装不;)

不要使用执行所有操作的单个对象,而是按照单一责任原则将您的逻辑分离到几个类和模块中。在您的示例中,很明显的提取候选是AI,您似乎部分意识到了这一点,如以下评论所示:

代码语言:javascript
复制
 # AI components

这种提取通常要求您传递(作为参数)以前仅可用的数据,但使代码更容易阅读和重构。例如,您的AI类可以使用#player=方法将其设置为:X:O,而#next_move方法则可以接受当前的游戏状态(很可能是棋盘),并返回AI在移动时所占用的字段数。

代码语言:javascript
复制
class AI
  attr_accessor :player     

  def next_move(board)
    # ... code code code ...
    return 5 # or something else
    # ... some more code ...
  end
end

提取AI将允许您更容易地修改您的程序,如果(例如)您希望允许观看两个CPU播放器之间的比赛。

票数 4
EN

Code Review用户

发布于 2015-09-29 13:47:52

determine_player可以大大简化:

代码语言:javascript
复制
def determine_player(player)
  player.to_s
end

我很难理解:

代码语言:javascript
复制
return @board[idx] = 'O' if @board[idx].is_a? Fixnum

您是returning并分配一个值,而根据单个响应原则,您可以返回或设置一个值。

票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/105996

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档