yasuhito/text2048

View on GitHub
text2048.org

Summary

Maintainability
Test Coverage
#+TITLE: Text2048
* 初期化
#+NAME: board_initialize
#+BEGIN_SRC ruby
  def initialize(tiles = Array.new(4, Array.new(4)), score = 0)
    @all_tiles = tiles.hashinize
    @score = score
  end
#+END_SRC
* タイルの移動
** 右
#+NAME: board_right
#+BEGIN_SRC ruby
  # @macro move
  def right
    board, score = to_a.reduce([[], @score]) do |(rows, sc), each|
      row, row_sc = each.right
      [rows << row, sc + row_sc]
    end
    new_board(board, score)
  end
#+END_SRC

** 左
#+NAME: board_left
#+BEGIN_SRC ruby
  # @macro move
  def left
    flip_horizontal { right }
  end
#+END_SRC

** 上
#+NAME: board_up
#+BEGIN_SRC ruby
  # @macro move
  def up
    transpose { left }
  end
#+END_SRC

#+NAME: board_transpose
#+BEGIN_SRC ruby
  def transpose(&block)
    board = transposed_board.instance_eval(&block)
    new_board(board.to_a.transpose, board.score)
  end
#+END_SRC

#+NAME: board_transposed_board
#+BEGIN_SRC ruby
  def transposed_board
    new_board(to_a.transpose, @score)
  end
#+END_SRC

** 下
#+NAME: board_down
#+BEGIN_SRC ruby
  # @macro move
  def down
    transpose { right }
  end
#+END_SRC

* 勝ち負けの判定
すべてのタイルの中で値が 2048 以上のものがあれば勝ち.

#+NAME: board_win_q
#+BEGIN_SRC ruby
  def win?
    @all_tiles.any? { |_key, each| each.to_i >= 2048 }
  end
#+END_SRC

上下左右に動かしてみて, タイルが一つも消えずに 16 個残っていたら手詰まり.

#+NAME: board_lose_q
#+BEGIN_SRC ruby
  def lose?
    right.left.up.down.tiles.size == 4 * 4
  end
#+END_SRC

* 盤の状態
** すべてのタイル
#+NAME: board_tiles
#+BEGIN_SRC ruby
  # @return [Array<Tile>] the list of tiles
  def tiles
    @all_tiles.select { |_key, each| each.to_i > 0 }
  end
#+END_SRC

** 消えたタイル
#+NAME: board_merged_tiles
#+BEGIN_SRC ruby
  # @return [Array] the list of +[row, col]+ of the merged tiles
  def merged_tiles
    find_tiles :merged
  end
#+END_SRC

** 生成されたタイル
#+NAME: board_generated_tiles
#+BEGIN_SRC ruby
  # @return [Array] the list of +[row, col]+ of the newly generated tiles
  def generated_tiles
    find_tiles :generated
  end
#+END_SRC

** タイルを生成できるか?
#+NAME: board_generate_q
#+BEGIN_SRC ruby
  # Need to generate a new tile?
  # @param other [Board] the previous {Board} object
  # @return [Boolean] generate a new tile?
  def generate?(other)
    to_a != other.to_a
  end
#+END_SRC

** タイルを一枚生成
#+NAME: board_generate
#+BEGIN_SRC ruby
  # Generates a new tile
  # @return [Board] a new board
  def generate
    tiles = @all_tiles.dup
    tiles[sample_empty_tile] = Tile.new(rand < 0.9 ? 2 : 4, :generated)
    new_board(tiles, @score)
  end
#+END_SRC
* ソースコード
#+BEGIN_SRC ruby :tangle lib/text2048/board.rb :padline no :noweb yes
  # encoding: utf-8

  require 'text2048/monkey_patch/array'
  require 'text2048/monkey_patch/hash'
  require 'text2048/tile'

  # This module smells of :reek:UncommunicativeModuleName
  module Text2048
    # 2048 game board
    class Board
      # @return [Number] returns the current score
      attr_reader :score

      <<board_initialize>>

      # @!group Move

      # @!macro [new] move
      #   Move the tiles to the $0.
      #   @return [Board] returns a new board

      <<board_right>>

      <<board_left>>

      <<board_up>>

      <<board_down>>

      # @!endgroup

      # @!group Tiles

      <<board_tiles>>

      <<board_merged_tiles>>

      <<board_generated_tiles>>

      <<board_generate_q>>

      <<board_generate>>

      # @!endgroup

      # @!group Win/Lose

      <<board_win_q>>

      <<board_lose_q>>

      # @!endgroup

      # @!group Conversion

      # @return [Array] a 2D array of tiles.
      def to_a
        [0, 1, 2, 3].map { |each| row(each) }
      end

      # @!endgroup

      private

      def flip_horizontal(&block)
        board = flipped_board.instance_eval(&block)
        new_board(board.to_a.map(&:reverse), board.score)
      end

      def flipped_board
        new_board(to_a.map(&:reverse), @score)
      end

      <<board_transpose>>

      <<board_transposed_board>>

      def empty_tiles
        @all_tiles.select { |_key, each| each.to_i == 0 }
      end

      def sample_empty_tile
        fail if empty_tiles.empty?
        empty_tiles.keys.shuffle.first
      end

      def new_board(tiles, score)
        self.class.new(tiles, score)
      end

      def find_tiles(status)
        @all_tiles.select { |_key, each| each.status == status }.keys
      end

      def row(index)
        [index].product([0, 1, 2, 3]).map { |each| @all_tiles[each] }
      end
    end
  end
#+END_SRC

* テストコード
#+BEGIN_SRC ruby :tangle spec/text2048/board_spec.rb :padline no :noweb yes
  # encoding: utf-8

  require 'text2048'

  describe Text2048::Board do
    Given(:board) do
      initial_tiles ? Text2048::Board.new(initial_tiles) : Text2048::Board.new
    end
    Invariant { board.score == 0 }
    Invariant { board.generated_tiles.empty? }
    Invariant { board.merged_tiles.empty? }

    context 'with no arguments' do
      Given(:initial_tiles) {}

      describe '#tiles' do
        When(:tiles) { board.tiles }
        Then { tiles.empty? }
      end

      describe '#generate' do
        When(:new_board) { board.generate }
        Then { new_board.generated_tiles.size == 1 }
      end

      describe '#generate?' do
        context 'with empty board' do
          Given(:other) { Text2048::Board.new }
          When(:result) { board.generate?(other) }
          Then { result == false }
        end
      end

      describe '#win?' do
        When(:result) { board.win? }
        Then { result == false }
      end

      describe '#lose?' do
        When(:result) { board.lose? }
        Then { result == false }
      end

      describe '#right' do
        When(:result) { board.right }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.score == 0 }
      end

      describe '#left' do
        When(:result) { board.left }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.score == 0 }
      end

      describe '#up' do
        When(:result) { board.up }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.score == 0 }
      end

      describe '#down' do
        When(:result) { board.down }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.score == 0 }
      end

      describe '#to_a' do
        When(:result) { board.to_a }
        Then do
          result == [[nil, nil, nil, nil],
                     [nil, nil, nil, nil],
                     [nil, nil, nil, nil],
                     [nil, nil, nil, nil]]
        end
      end
    end

    context 'with four 2s in diagonal' do
      Given(:initial_tiles) do
        [[2, nil, nil, nil],
         [nil, 2, nil, nil],
         [nil, nil, 2, nil],
         [nil, nil, nil, 2]]
      end

      describe '#tiles' do
        When(:tiles) { board.tiles }
        Then { board.tiles.size == 4 }
        And { board.tiles[[0, 0]] == 2 }
        And { board.tiles[[1, 1]] == 2 }
        And { board.tiles[[2, 2]] == 2 }
        And { board.tiles[[3, 3]] == 2 }
      end

      describe '#generate' do
        When(:new_board) { board.generate }
        Then { new_board.generated_tiles.size == 1 }
      end

      describe '#generate?' do
        context 'with empty board' do
          Given(:other) { Text2048::Board.new }
          When(:result) { board.generate?(other) }
          Then { result == true }
        end
      end

      describe '#win?' do
        When(:result) { board.win? }
        Then { result == false }
      end

      describe '#lose?' do
        When(:result) { board.lose? }
        Then { result == false }
      end

      describe '#right' do
        When(:result) { board.right }

        Then do
          result.to_a == [[nil, nil, nil, 2],
                          [nil, nil, nil, 2],
                          [nil, nil, nil, 2],
                          [nil, nil, nil, 2]]
        end
        And { result.merged_tiles.empty? }
        And { result.score == 0 }
      end

      describe '#left' do
        When(:result) { board.left }

        Then do
          result.to_a == [[2, nil, nil, nil],
                          [2, nil, nil, nil],
                          [2, nil, nil, nil],
                          [2, nil, nil, nil]]
        end
        And { result.merged_tiles.empty? }
        And { result.score == 0 }
      end

      describe '#up' do
        When(:result) { board.up }

        Then do
          result.to_a == [[2, 2, 2, 2],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.merged_tiles.empty? }
        And { result.score == 0 }
      end

      describe '#down' do
        When(:result) { board.down }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [2, 2, 2, 2]]
        end
        And { result.merged_tiles.empty? }
        And { result.score == 0 }
      end

      describe '#to_a' do
        When(:result) { board.to_a }
        Then do
          result == [[2, nil, nil, nil],
                     [nil, 2, nil, nil],
                     [nil, nil, 2, nil],
                     [nil, nil, nil, 2]]
        end
      end
    end

    context 'with six 2s that can be merged' do
      Given(:initial_tiles) do
        [[2, nil, 2, nil],
         [nil, 2, nil, nil],
         [nil, 2, nil, 2],
         [nil, nil, nil, 2]]
      end

      describe '#tiles' do
        When(:tiles) { board.tiles }
        Then { board.tiles.size == 6 }
        And { board.tiles[[0, 0]] == 2 }
        And { board.tiles[[0, 2]] == 2 }
        And { board.tiles[[1, 1]] == 2 }
        And { board.tiles[[2, 1]] == 2 }
        And { board.tiles[[2, 3]] == 2 }
        And { board.tiles[[3, 3]] == 2 }
      end

      describe '#generate' do
        When(:new_board) { board.generate }
        Then { new_board.generated_tiles.size == 1 }
      end

      describe '#generate?' do
        context 'with empty board' do
          Given(:other) { Text2048::Board.new }
          When(:result) { board.generate?(other) }
          Then { result == true }
        end
      end

      describe '#win?' do
        When(:result) { board.win? }
        Then { result == false }
      end

      describe '#lose?' do
        When(:result) { board.lose? }
        Then { result == false }
      end

      describe '#right' do
        When(:result) { board.right }

        Then do
          result.to_a == [[nil, nil, nil, 4],
                          [nil, nil, nil, 2],
                          [nil, nil, nil, 4],
                          [nil, nil, nil, 2]]
        end
        And { result.to_a[0][3].merged? }
        And { result.to_a[2][3].merged? }
        And { result.merged_tiles == [[0, 3], [2, 3]] }
        And { result.score == 8 }
      end

      describe '#left' do
        When(:result) { board.left }

        Then do
          result.to_a == [[4, nil, nil, nil],
                          [2, nil, nil, nil],
                          [4, nil, nil, nil],
                          [2, nil, nil, nil]]
        end
        And { result.to_a[0][0].merged? }
        And { result.to_a[2][0].merged? }
        And { result.merged_tiles == [[0, 0], [2, 0]] }
        And { result.score == 8 }
      end

      describe '#up' do
        When(:result) { board.up }

        Then do
          result.to_a == [[2, 4, 2, 4],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil]]
        end
        And { result.to_a[0][1].merged? }
        And { result.to_a[0][3].merged? }
        And { result.merged_tiles == [[0, 1], [0, 3]] }
        And { result.score == 8 }
      end

      describe '#down' do
        When(:result) { board.down }

        Then do
          result.to_a == [[nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [nil, nil, nil, nil],
                          [2, 4, 2, 4]]
        end
        And { result.to_a[3][1].merged? }
        And { result.to_a[3][3].merged? }
        And { result.merged_tiles == [[3, 1], [3, 3]] }
        And { result.score == 8 }
      end

      describe '#to_a' do
        When(:result) { board.to_a }
        Then do
          result == [[2, nil, 2, nil],
                     [nil, 2, nil, nil],
                     [nil, 2, nil, 2],
                     [nil, nil, nil, 2]]
        end
      end
    end

    context 'with one 2048 tile' do
      Given(:initial_tiles) do
        [[nil, nil, nil, nil],
         [nil, nil, nil, nil],
         [nil, 2048, nil, nil],
         [nil, nil, nil, nil]]
      end

      describe '#win?' do
        When(:result) { board.win? }
        Then { result == true }
      end

      describe '#lose?' do
        When(:result) { board.lose? }
        Then { result == false }
      end
    end

    context 'with 16 tiles which cannot be merged' do
      Given(:initial_tiles) do
        [[2, 4, 8, 16],
         [4, 8, 16, 32],
         [8, 16, 32, 64],
         [16, 32, 64, 128]]
      end

      describe '#tiles' do
        When(:result) { board.tiles }
        Then { result.size == 16 }
      end

      describe '#generate' do
        When(:result) { board.generate }
        Then { result == Failure(RuntimeError) }
      end

      describe '#win?' do
        When(:result) { board.win? }
        Then { result == false }
      end

      describe '#lose?' do
        When(:result) { board.lose? }
        Then { result == true }
      end

      describe '#right' do
        When(:result) { board.right }
        Then { result.to_a == board.to_a }
      end

      describe '#left' do
        When(:result) { board.left }
        Then { result.to_a == board.to_a }
      end

      describe '#up' do
        When(:result) { board.up }
        Then { result.to_a == board.to_a }
      end

      describe '#down' do
        When(:result) { board.down }
        Then { result.to_a == board.to_a }
      end

      describe '#to_a' do
        When(:result) { board.to_a }
        Then do
          result == [[2, 4, 8, 16],
                     [4, 8, 16, 32],
                     [8, 16, 32, 64],
                     [16, 32, 64, 128]]
        end
      end
    end
  end
#+END_SRC