你建议从结构、逻辑等方面进行哪些改变?
# 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发布于 2015-09-29 17:34:48
上面的代码是害怕增加类的例子。这有两个症状:Game是一个上帝对象,以及对原语的过度依附(或者它是Ruby,过度依赖于内置类)。有趣的是,即使在这样简单的例子中,我们也可以看到为什么这些都是坏事。
考虑这两种方法(我现在减少了一些不重要的代码):
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现在是字段数组):
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实际打印的内容。这将适用于代码的其他部分,即
if @board.include?(position)与
if @board[position].empty?在使用类时,我们可以描述正在做的事情,原语迫使我们描述事情是如何完成的。另外,可能更重要的好处是,即使我们改变了字段类的工作方式,像#draw?这样的方法也会同样工作--我们实现了封装封装。
上帝的对象是众所周知的代码气味。想想看:如果您只有一个类,即只实例化一次(而且您需要实例化),那么您甚至不需要对象和类--只需将您的@s更改为$s,以便您的状态保持在全局范围内,而不是对象的状态(这并没有什么不同,因为您的所有代码都使用相同的状态!),您的代码将同样工作。您可能知道为什么globals是不明智的,正如您可以看到您的代码工作在全局,只是假装不;)
不要使用执行所有操作的单个对象,而是按照单一责任原则将您的逻辑分离到几个类和模块中。在您的示例中,很明显的提取候选是AI,您似乎部分意识到了这一点,如以下评论所示:
# AI components这种提取通常要求您传递(作为参数)以前仅可用的数据,但使代码更容易阅读和重构。例如,您的AI类可以使用#player=方法将其设置为:X或:O,而#next_move方法则可以接受当前的游戏状态(很可能是棋盘),并返回AI在移动时所占用的字段数。
class AI
attr_accessor :player
def next_move(board)
# ... code code code ...
return 5 # or something else
# ... some more code ...
end
end提取AI将允许您更容易地修改您的程序,如果(例如)您希望允许观看两个CPU播放器之间的比赛。
发布于 2015-09-29 13:47:52
determine_player可以大大简化:
def determine_player(player)
player.to_s
end我很难理解:
return @board[idx] = 'O' if @board[idx].is_a? Fixnum您是returning并分配一个值,而根据单个响应原则,您可以返回或设置一个值。
https://codereview.stackexchange.com/questions/105996
复制相似问题