LoginSignup
1
1

More than 3 years have passed since last update.

Rで原始的なマルバツゲーム(CPUなし)を作成する

Last updated at Posted at 2021-02-03

プログラミング初心者が取り組むことの多いボードゲームの作成(マルバツゲーム、五目並べ、オセロ、将棋、etc.)。Pythonを使った同様の記事は見つかったものの、Rによるボードゲームの作成を初心者向けに解説する記事がなかったため、今回初心者の私が手始めにRでマルバツゲームの作成(コンソールで動作、CPUなし)に挑戦してみようと思います。初心者の私が同じく初心者の皆様に参考になればと思ったので説明は冗長かもしれませんが、Rによるプログラミングの第一歩を始めてみたいという方の参考になれば幸いです。

環境

  • R (ver. 4.0.2)

  • R studio (ver. 1.3.1056)

特殊なライブラリ等も使用しませんので、旧バージョン(ver. 3)でも動作するはずです。とにかくRだけで動かします。

作っていくもの

一般にマルバツゲームといえば3×3のマスに先手から順に〇と×を書き込んでいき、縦横ナナメのいずれかがそろえば勝ちという単純なゲームです。ゲームとして動かすために次のものを作っていきます。

1. ゲームを行う3×3の表
2. 〇と×(実際はそれの代用)を書き込む関数
3. どちらかが勝利したことを判断する関数
4. ゲームを進行するプログラム

では順に作っていきましょう!

作成

1. ゲームを行う3×3の表

マルバツゲームは一般に3×3のマスで行うゲームですので、これを行列表示で作成します。今回は0を「何も描かれていない状態」1を「〇が描かれている状態」-1を「×が描かれている状態」と考えます。ゲームが進行していく中でありうるマスの状態はこの3つのうちのいずれかですので、ゲームを表現するには十分です。

matrix(data = 0, ncol = 3, nrow = 3)

結果はこんな感じ。

#>     [,1] [,2] [,3]
#>[1,]    0    0    0
#>[2,]    0    0    0
#>[3,]    0    0    0

これで3×3の表ができました。これからゲームを作るにあたって、まっさらな表を作る関数を以下のように定義しておきます。

reset_table <- function(){
  game_table <<- matrix(data = 0, ncol = 3, nrow = 3)
}

(注:<<-はグローバル変数を指定する代入演算子です。<-で書いてしまうとreset_table()の外ではgame_tableを指定することができません。この後に作る関数でも使用していきます。)

2. 〇と×(実際はそれの代用)を書き込む関数

今回は〇を意味するものとして1、×を意味するものとして-1を表に書くものとするので、それらを書き込む関数を作ります。表の指定した場所に1もしくは-1を入れることができればよいので、

十分ではないコード
maru <- function(x,y){
  game_table[x,y] <<- 1
}

このように書けば事足りそうですが、このように定義をしてしまうとすでに〇や×が書かれているはずのマスにも上書きすることができてしまいます。つまり、関数の中には表の中で書きたい部分が空欄(すなわち0)となっている場合にのみ書き込めるようにしなくてはなりません。

修正されたコード
maru <- function(x, y){
  if(game_table[x,y]==0){
    game_table[x,y] <<- 1
    print(game_table)
  }else{
    print("そこに描くことはできません!もう一度入力してください。")
  }
}

これで空欄ではないマスに書き込もうとした場合は「そこに描くことはできません!もう一度入力してください。」と表示させることができますね!×を書き込む関数も同様に作ります。

batsu <- function(x,y){
  if(game_table[x,y]==0){
    game_table[x,y] <<- -1
    print(game_table)
  }else{
    print("そこに描くことはできません!もう一度入力してください。")
  }
}

作った関数の動き方を確認しておきましょう。

reset_table() # 新しい3×3の表を用意
maru(2,2)

#>     [,1] [,2] [,3]
#>[1,]    0    0    0
#>[2,]    0    1    0
#>[3,]    0    0    0

batsu(1,1)

#>     [,1] [,2] [,3]
#>[1,]   -1    0    0
#>[2,]    0    1    0
#>[3,]    0    0    0

maru(1,1)

#>[1] "そこに描くことはできません!もう一度入力してください。"

計画通り動いてますね!

3. どちらかが勝利したことを判断する関数

ゲームをするわけですから、どちらかが勝利条件を満たした際にはどちらが勝利したのか判定してゲームをストップしなくてはなりません。よって勝利したことを判定する関数を作ります。

〇×ゲームで勝敗がつくのはどちらかの記号が縦・横・ナナメいずれかに3つ並んだときです。一気に判定をするのは難しいので、①縦にそろっているかチェックする関数②横にそろっているかチェックする関数、そして③ナナメにそろっているかチェックする関数の3つを作成し、それらを結合して勝利したことを判断する関数を作ります。まず、①縦にそろっているかチェックする関数から考えてみます。

今回の表現では〇を1と表現しているので、縦に〇が3つそろったとするとその列の和は3になり、逆に和が3になる場合は列の要素がすべて1である以外にありません。(一方、×がそろえば和は-3になります。)このことをうまく利用しましょう。各列がそれぞれ〇または×が3つそろっているかどうかチェックして値を返してもらいましょう。

col_win <- function(game_table){
  col_check <- c(rep(NA,3))      #揃っているか判断のため空のベクトルを用意
  for(i in 1:3){                 #3つの列について
    if((sum(game_table[,i])==3)|(sum(game_table[,i])==-3)){ #列の要素の和が3もしくは-3であるなら
      col_check[i] <- TRUE         #空のベクトルにTRUEを代入して
      win_side <<- game_table[1,i] #勝ったほうの値を保存しておく
    }else{
      col_check[i] <- FALSE #それ以外ならば空のベクトルにFALSEを代入
    }
  }
  return(col_check) #揃っているか判断したベクトルを返す
}

②横にそろっているかチェックする関数もこれを応用して同様に書いていきます。

row_win <- function(game_table){
  row_check <- c(rep(NA,3))
  for(i in 1:3){
    if((sum(game_table[i,])==3)|(sum(game_table[i,])==-3)){
      row_check[i] <- TRUE
      win_side <<- game_table[i,1]
    }else{
      row_check[i] <- FALSE
    }
  }
  return(row_check)
}

さて最後に③ナナメに揃っているかチェックする関数をつくります。この場合ナナメに揃うパターンは3×3行列が作り出す正方形の対角線の要素をチェックすればいいため、直接それぞれの要素を取り出してきてチェックをしてもらいます。1

cross_win <- function(game_table){
  cross_check <- c(rep(FALSE,2)) #デフォルトでFALSEとなっているベクトルを用意
  if((sum(game_table[1,1],game_table[2,2],game_table[3,3])==3)|
     (sum(game_table[1,1],game_table[2,2],game_table[3,3])==-3)){ #左上から右下に向かうナナメに揃っているなら
    cross_check[1] <- TRUE       #ベクトルの1つめをTRUEに書き換え
    win_side <<- game_table[1,1] #勝ったほうの値を保存
  }else if((sum(game_table[3,1],game_table[2,2],game_table[1,3])==3)|
           (sum(game_table[3,1],game_table[2,2],game_table[1,3])==-3)){ #もう一方のナナメに揃っているなら
    cross_check[2] <- TRUE       #ベクトルの2つめをTRUEに書き換え
    win_side <<- game_table[3,1] #勝ったほうの値を保存
  }
  return(cross_check) #判断してもらったベクトルを返す
}

以上の関数は次のように動作します。仮に以下のような盤面が存在したとします。

game_table
#>     [,1] [,2] [,3]
#>[1,]   -1    1    0
#>[2,]    0    1    0
#>[3,]    0    1   -1

うーん、×側は早くこの勝負を終えたかったのでしょうか、ストレート負けしています。なんにせよこの盤面を作った関数に入れると以下のように動きます。

col_win(game_table)
#>[1] FALSE  TRUE FALSE
row_win(game_table)
#>[1] FALSE FALSE FALSE
cross_win(game_table)
#>[1] FALSE FALSE

TRUEFALSEは論理演算子で、数値計算(例えばsum())ではそれぞれ10として扱われます。このことを利用して最終的に盤面がどちらかの勝利条件を満たしているかどうかを判定します。

win_check <- function(game_table){
 if(sum(c(col_win(game_table),row_win(game_table),cross_win(game_table)))>=1){
    return(TRUE)  #列が1つ以上揃っているならTRUEを返し
  }else{
    return(FALSE) #揃っていなければFALSEを返す
  }
}

win_check(game_table)
#>[1] TRUE

TRUEは最終的にどこかで勝利条件が満たされていることを示します。

(注: 以上で作成した関数では、正しく〇×ゲームを遊んだ時にありえない盤面かどうかは判断することができません。例えば、〇と×が同時に揃っている状況ではどちらかを勝者と判断してしまいます。しかし次のステップで全体のゲームを正しく進行できるならば今回は問題になりません。)

4. ゲームを進行するプログラム

それでは最後に今まで作った関数を組み合わせてマルバツゲームを進行するプログラムを関数にして書きましょう。同時に前のステップで判断しなかった引き分けについても定義しておきます。いくつかの点については簡単に何を行っているか解説を後述します。

game_marubatsu <- function(){
  reset_table() #新しい表を用意
  print(game_table)
  repeat{ #これからの関数を繰り返す(解説:1)
    if(!(win_check(game_table))){ #勝利条件が満たされていないなら以下を実行
      if(prod(game_table)!=0){ #引き分け条件を満たすなら(解説:2)
        print("引き分けです")
        break #ここに来たら繰り返しを停止(解説:1)
      }else if(sum(game_table)==0){ #先手番を判断(解説:3)
        print("先手番です。〇を描きたいマスを入力してください")
        print("例: 下段左に描きたい場合'3 1'と入力してください")
        maru_point <- readline() #コンソールでの入力を要求(解説:4)
        maru_numeric <- as.numeric(unlist(strsplit(maru_point, "[ ,]")))
        maru(maru_numeric[1],maru_numeric[2]) #コンソールで入力された値を手として実現
      }else{ #先手番でないなら(以下先手番と同様)
        print("後手番です。×を描きたいマスを入力してください")
        print("例: 下段左に描きたい場合'3 1'と入力してください")
        batsu_point <- readline()
        batsu_numeric <- as.numeric(unlist(strsplit(batsu_point, "[ ,]")))
        batsu(batsu_numeric[1],batsu_numeric[2])
      }
    }else{ #勝利の条件が満たされているなら
      if(win_side == 1){ #先手勝利なら
        print("先手の勝利です")
        break #繰り返しを停止(解説:1)
      }else{ #先手勝利でないなら
        print("後手の勝利です")
        break #繰り返しを停止(解説:1)
      }
    }
  }
}
  • 解説1: repeat{}breakについて

repeat{}{}の中にある関数をbreakに行きつくまで繰り返し実行します。今回のようにどちらかが勝利するまで手番を交互に繰り返す際にはぴったりです。

  • 解説2: 引き分け条件について

ここでの引き分け条件は勝利条件を満たしていないが、すべてのマスが埋まっている場合引き分けと判断するようになっています。ここで「すべてのマスが埋まっている」ことはすべてのマスが0以外であることです。ここは行列の全ての要素の積を求めるprod()を用い、prod(game_table)0でなければすべてのマスが埋まっていると判断できます。

  • 解説3: 先手番と後手番の判断方法

〇と×を1-1で表現しているため、先手番(〇:1)が終わると行列すべての要素の総和が1となり、次に後手番(×:-1)が終わると総和が0になるはずです。これを条件分岐で判断します。

  • 解説4: readline()でコンソールに入力された値を使う

readline()を用いると関数の実行の途中でコンソールに値を入力してその値を反映させることができます。

では完成した関数を実行してマルバツゲームをうごかしてみましょう~

game_marubatsu()

今後の展望

今回はRのコンソールで動かせる原始的なマルバツゲーム(っぽいもの)を作成していきました。今回作ったものは初心者も自力で作れるごく簡単なものでしかありませんが、今後

  • ゲームを発展させる(五目並べ、オセロ etc.)
  • CPUの実装
  • Webブラウザでの実装
  • 他の言語での再現

等々、しっかりと広がりがあるのでとてもいい第一歩なのではないかと思います。個人的には今後CPUの実装から手を付けてみようと思います。Rでのプログラミング記事は少ないようですので、また進展がありましたら投稿します。少しでも参考になれば幸いです。


  1. diag()などの関数を用いることもできますが、2つしか判断しないので書ききってしまってもこの程度です。ナナメの判断はまた五目並べやオセロでも問題になるので、その際は全部書き出すコードでは対応できません。(時間と根気が無尽蔵にある人を除き…) 

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1