餅屋LGTM

餅つきのあとの静けさ

Rubyでオセロ 〜その1〜

自分の考えたことを振り返りながら順番に書いていく、あくまで自分の過程なのでまったくもってスマートじゃない
かかった時間は対人対戦ができるまでに1日(約4〜5時間), COM対戦、棋譜保存と読み込みに1日, 待ったに1日くらい
本来はフローチャートを書いてみたいな事をするのが正しいと思うけれども自分は一気に書いて見なおししてちょっと綺麗にしてと進めていったので割とぐちゃぐちゃ

とりあえず二人対戦できるところまで

1. 盤を作る

  1. 10×10の二次元配列で盤の座標を表す(8×8ではないのは8×8の盤の外側に壁を設けるため)
  2. 座標の状態を(空き,黒石,白石,外周*1 )で表す
    (この時黒石を1,白石を-1のように反数*2で表すと返す処理などが楽)
  3. 盤を表示する(番兵の部分は表示しない)
# -*- coding: utf-8 -*-

#定数の定義
BOARD_SIZE = 8 #盤のサイズ(8*8)
EMPTY = 0 #空きマス
BLACK = 1 #黒石のマス
WHITE = -1 #白石のマス
WALL = 2 #番兵(ひっくり返すときに使う)

ROW_NUM = {"a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7, "h" => 8} #列番号の変換
COL_NUM = {"1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5, "6" => 6, "7" => 7, "8" => 8} #行番号の変換

def new_board
  #盤面を表す二次元配列の作成
  @board = Array.new(BOARD_SIZE+2){Array.new(BOARD_SIZE+2,EMPTY)}
  #番兵の配置
  @board[0].fill(WALL)
  @board[9].fill(WALL)
  @board.each do |col|
    col[0] = WALL
    col[9] = WALL
  end
  #石の初期配置
  @board[4][4] = WHITE
  @board[5][5] = WHITE
  @board[5][4] = BLACK
  @board[4][5] = BLACK
end

#現在の盤の状態を表示
def show_board
  #行番号
  print("\n  #{ROW_NUM.keys.join(" ")}\n")
  @board.slice(1...-1).each_with_index do |col, i|
    #列番号
    print(COL_NUM[(i+1).to_s])
    col.each do |row|
      #マスの状態
      case row
      when EMPTY
        print "\e[32m"
        print("")
        print "\e[0m"
      when BLACK
        print("")
      when WHITE
        print("")
      end
    end
    print("\n")
  end
end

new_board
show_board

f:id:umaz1051:20170209123612p:plain

2. 石を置く

1. ゲームの開始
  1. 盤の作成
  2. ターン数の初期化
  3. 石の初期化(黒石からスタート)
  4. 石を置く
  5. 盤を見せる
  6. ゲームの終了(60手になったら終了)
...

#ゲームの進行
def start_game
  new_board
  show_board
  @turn = 0 #ターン数
  @stone_color = BLACK #黒石からスタート
  while true
    put_stone
    show_board
  end
end
2. 返せるかどうかの判定
  1. 返せる方向8方向の定数を定義
  2. 各方向に相手の色の石があり、かつその先に自分の色の石がある場合返せる
#ひっくり返せる方向
UPPER_LEFT = 1
UPPER = 2
UPPER_RIGHT = 3
RIGHT = 4
LOWER_RIGHT = 5
LOWER = 6
LOWER_LEFT = 7
LEFT = 8
def turnable_direction(col, row)
  direction = [] #返せる方向を格納する配列
  #左上
  if @board[col-1][row-1] == -@stone_color #相手の色がある場合
    i = 2
    while @board[col-i][row-i] == -@stone_color #相手の色が続く間
      i += 1
    end
    if @board[col-i][row-i] == @stone_color #相手の色に続くのが自分の色の場合
      direction.push(UPPER_LEFT)
    end
  end
  if @board[col-1][row] == -@stone_color #上
    i = 2
    while @board[col-i][row] == -@stone_color
      i += 1
    end
    if @board[col-i][row] == @stone_color
      direction.push(UPPER)
    end
  end
  if @board[col-1][row+1] == -@stone_color #右上
    i = 2
    while @board[col-i][row+i] == -@stone_color
      i += 1
    end
    if @board[col-i][row+i] == @stone_color
      direction.push(UPPER_RIGHT)
    end
  end
  if @board[col][row+1] == -@stone_color #右
    i = 2
    while @board[col][row+i] == -@stone_color
      i += 1
    end
    if @board[col][row+i] == @stone_color
      direction.push(RIGHT)
    end
  end
  if @board[col+1][row+1] == -@stone_color #右下
    i = 2
    while @board[col+i][row+i] == -@stone_color
      i += 1
    end
    if @board[col+i][row+i] == @stone_color
      direction.push(LOWER_RIGHT)
    end
  end
  if @board[col+1][row] == -@stone_color #下
    i = 2
    while @board[col+i][row] == -@stone_color
      i += 1
    end
    if @board[col+i][row] == @stone_color
      direction.push(LOWER)
    end
  end
  if @board[col+1][row-1] == -@stone_color #左下
    i = 2
    while @board[col+i][row-i] == -@stone_color
      i += 1
    end
    if @board[col+i][row-i] == @stone_color
      direction.push(LOWER_LEFT)
    end
  end
  if @board[col][row-1] == -@stone_color #左
    i = 2
    while @board[col][row-i] == -@stone_color
      i += 1
    end
    if @board[col][row-i] == @stone_color
      direction.push(LEFT)
    end
  end
  return direction
end
3. 石を置ける場所の一覧の取得
  1. すべての空きマスに対して返せるかどうかの判定を行う
  2. 返せる方向が存在する場合そのマスを保存する
# 石を置くことができるマスの一覧
def get_turnable_cells
  turnable_cells = []
  @board.each_with_index do |col, i|
    col.each_with_index do |row, j|
      if row == EMPTY #空きマス
        turnable_direction = turnable_direction(i,j)
        if turnable_direction.size != 0 #ひっくり返せる方向が存在する=石を置けるマス
          turnable_cells.push([i,j]) #座標を格納
        end
      end
    end
  end
  return turnable_cells
end
4. 石を置く
  1. 指し手の入力を受ける
  2. マスが返せる場所であれば石を置く
  3. ターン数を増やす
  4. 石の色を変える
...

#石を置く
def put_stone
  turnable_cells = get_turnable_cells
  print("#{@turn+1}手目: ")
  move = gets.chomp!  #着手の入力
  if move =~ /[a-h][1-8]/ #正しいマスの時
    cell = move.split("")
    col = COL_NUM[cell[1]]
    row = ROW_NUM[cell[0]]
    cell = [col, row]
    # 着手がひっくり返せるマスの一覧の中にある場合
    if turnable_cells.include?(cell)
      @board[col][row] = @stone_color
      @stone_color = -@stone_color
      @turn += 1
    else
      print("そのマスには打つことはできません\n")
    end
  else
    print("正しいマスを指定してください\n")
    put_stone
  end
end
start_game

f:id:umaz1051:20170209123616p:plain

3. 石を返す

選択されたマスから返せる方向へ相手の石の色の間自分の石の色に変える

...

def put_stone
  ...
  if move =~ /[a-h][1-8]/
    ...
    if turnable_cells.include?(cell)
      @board[col][row] = @stone_color
      reverse_stone(col, row) #追加
      @stone_color = -@stone_color
      @turn += 1
    else
      ...
    end
  else
    ...
  end
end

#石をひっくり返す
def reverse_stone(col, row) #石をおいた位置
  turn_direction = turnable_direction(col, row) #方向と数を取得
  turn_direction.each do |direction|
    case direction
    when UPPER_LEFT
      i = 1
      while @board[col-i][row-i] == -@stone_color #相手の色が続くまで
        @board[col-i][row-i] = @stone_color #自分の色にする
        i += 1
      end
    when UPPER
      i = 1
      while @board[col-i][row] == -@stone_color
        @board[col-i][row] = @stone_color
        i += 1
      end
    when UPPER_RIGHT
      i = 1
      while @board[col-i][row+i] == -@stone_color
        @board[col-i][row+i] = @stone_color
        i += 1
      end
    when RIGHT
      i = 1
      while @board[col][row+i] == -@stone_color
        @board[col][row+i] = @stone_color
        i += 1
      end
    when LOWER_RIGHT
      i = 1
      while @board[col+i][row+i] == -@stone_color
        @board[col+i][row+i] = @stone_color
        i += 1
      end
    when LOWER
      i = 1
      while @board[col+i][row] == -@stone_color
        @board[col+i][row] = @stone_color
        i += 1
      end
    when LOWER_LEFT
      i = 1
      while @board[col+i][row-i] == -@stone_color
        @board[col+i][row-i] = @stone_color
        i += 1
      end
    when LEFT
      i = 1
      while @board[col][row-i] == -@stone_color
        @board[col][row-i] = @stone_color
        i += 1
      end
    end
  end
end

f:id:umaz1051:20170209124337p:plain

4. ゲームの終了

ゲームの終了の条件は

  1. 60手経過
  2. 石を置ける場所がなくなる(=双方パス)
1. パスの判定

turnable_directionの返り値が0の場合石を置ける場所がないのでパス、相手のターンにする

...

def start_game
  ...
  @pass_count = 0
  ...
  while true
    ...

  end
end

def put_stone
  turnable_cells = get_turnable_cells
  if turnable_cells.size == 0
    print("パスしました\n")
    @pass_count += 1 #パスした時に増やす
    @stone_color = -@stone_color
  else
    if move =~ /[a-h][1-8]/
      ...
      if turnable_cells.include?(cell)
        ...
        @pass_count = 0 #石を置けたら戻す
      else
        ...
      end
    else
      ...
    end
  end
end
2. ゲームの終了
  1. ターン数が60になる
  2. 2連続でパスをする
MAX_TURN = 60
...

def start_game
  ...

  while true
    ...
    if @turn == MAX_TURN
      break
    end
    if @pass_count == 2
      break
    end
  end
end
3. 勝敗の判定

盤面の黒石の数と白石の数を数え多い方の勝ち、同じならば引き分け

COLOR = {BLACK => "", WHITE => ""} #色
...

def start_game
  ...

  while true
    ...
  end
  judge
end

...

def judge
  black = 0
  white = 0
  @board.each do |col|
    col.each do |row|
      case row
      when BLACK
        black += 1
      when WHITE
        white += 1
      end
    end
  end
  if black > white
    print("\n黒:#{black} 対 白:#{white} で黒の勝ち\n")
  elsif white > black
    print("\n黒:#{black} 対 白:#{white} で白の勝ち\n")
  else
    print("\n黒:#{black} 対 白:#{white} で引き分け\n")
  end
end

f:id:umaz1051:20170209155655p:plain

完成

毎回石の数を表示するようにしたり座標の選択を間違えたときに石を置ける座標を表示するようにしたりして完成

対人対戦できるオセロ · GitHub