石取りゲーム

先日のRuby勉強会のお題だったのですが、時間がなくて演習中にできなかったので、今日改めてやってみました。というかid:nshttskさんの成果(id:nshttsk:20070428:1177787956)をリファクタリングしてみました(賢いコンピュータの実装はサボりました)。リファクタリングのポイントは以下です。

  • テストし易そうなインターフェースにする(Mockオブジェクトを使ってでテストできる部分を増やす)。
  • (上にもからめて)入出力部分をまとめる。MVCモデルぽくする。

うーむ、やっぱりかえって複雑になってしまったような気がします。それにしてもメソッド名のセンスが悪いなぁ。我ながら。
コメントありましたらお願いします。どしどしお願いします。

#!/usr/bin/env ruby

module StoneTakingGame
  class Player
    def initialize(game, name)
      @game = game
      @name = name
    end
    attr_reader :name

    def how_many_stones
      raise "#{@name} must rewrite this method."
    end
  end

  class HumanPlayer < Player
    def initialize(game, name)
      super(game, name)
    end

    def how_many_stones
      @game.query_how_many_stones
    end
  end

  class RandomComputerPlayer < Player
    def initialize(game, name = 'Random Computer')
      super(game, name)
    end

    def how_many_stones
      rand(3) + 1
    end
  end

  class Board
    def initialize(nstones = rand(10) + 10)
      @nstones = nstones
    end
    attr_reader :nstones

    def take(n)
      @nstones -= n
    end

    def empty?
      @nstones < 1
    end
  end

  class View
    def query_game_config
      name = get_line('Input your name: ')
      name = 'Hoge' if name.empty?
      is_first = (get_line('Do you take stone at FIRST? [Y/n]: ') =~ /[Yy]/)
      [name, is_first]
    end

    def query_how_many_stones
      n = get_line('Input number of taking stone: ').to_i
      raise 'invalid number.' unless (1 .. 3).include?(n)
      n
    rescue
      puts "input between 1 and 3, OK?"
      retry
    end

    def disp_beggining_of_turn(player, board)
      puts "[#{player.name}'s turn]"
      s = "Number of stone: #{board.nstones}\n"
      board.nstones.times do |n|
        s << " " if n % 10 == 0 and n != 0
        s << "\n" if n % 20 == 0 and n != 0
        s << '*'
      end
      puts s
    end

    def disp_how_many_stones(player, n)
      puts "#{player.name} takes #{n} stone#{(n <= 1) ? '' : 's'}"
      puts
    end

    def disp_lost_player(player)
      puts "#{player.name} lost!"
    end

    private

    def get_line(prompt)
      $stdout.print prompt
      $stdout.flush
      gets.chomp
    end
  end

  class Game
    def initialize(view, board, players = [RandomComputerPlayer.new(self)])
      @view = view
      @board = board
      @players = players
    end

    def start
      name, is_first = @view.query_game_config
      @players.push HumanPlayer.new(self, name)
      @players.reverse! if is_first

      each_players do |player|
        @view.disp_beggining_of_turn(player, @board)
        n = player.how_many_stones
        @board.take(n)
        @view.disp_how_many_stones(player, n)
        if @board.empty?
          @view.disp_lost_player(player)
          break
        end
      end
    end

    def query_how_many_stones
      @view.query_how_many_stones # delegate to @view.
    end

    private

    def each_players
      loop { @players.each {|p| yield(p) } }
    end
  end
end

if $0 == __FILE__
  include StoneTakingGame
  Game.new(View.new, Board.new).start
end