こんにちは、usagi-sanです。
今回は、統計解析やデータ解析用のスクリプト言語であるR言語を用いてマインスイーパーを作成しました。
R言語は手続き型の言語であるため、クラスを定義することはほとんどありませんが、今回ゲームを作成するということでクラスを定義し、オブジェクト指向でスクリプトを書いてみました。
マインスイーパーは、次の画像のようにR言語のwindow上で遊ぶことができます。
マインスイーパーのプログラミングコードはこの記事の一番下でダウンロードできます。
クラスの定義の仕方の勉強したい方や興味のある方は、是非是非ダウンロードしてください。
クラス mineSweeper
マインスイーパーのアルゴリズムや描画するメソッドなどから成るクラスmineSweeperを紹介します。
クラスmineSweeperは次のフィールドとメソッドから構成されています。
フィールド
- n_row = "numeric"
- n_col = "numeric"
- n_bomb = "numeric"
- df = "data.frame"
- exploredTile = "data.frame"
- flaggedTile = "data.frame"
- gameEnd = "logical"
- mode = "character"
- remainingCells = "numeric"
メソッド
- Initialize = function()
- Start = function()
- Explore = function(x,y)
- GameStartPlot = function()
- GamePlot = function()
- MousePointer = function(x,y)
- ConvertMode = function(x, y)
- RemainingCells = function()
このクラスのインスタンスを生成し、メソッドをmain()で実行することで、マインスイーパーを実行することができます。
これらのフィールドとメソッドの意味や役割は次で紹介していきます。
フィールド
n_row
マインスイーパーの行の数を指定する。ゲーム開始時に指定した行数がn_rowに代入されます。
n_col
マインスイーパーの列の数を指定する。ゲーム開始時に指定した列数がn_colに代入されます。
n_bomb
マインスイーパーの地雷の数を指定する。ゲーム開始時に指定した地雷の数がn_bombに代入されます。
df
マインスイーパーの盤面の地雷の位置や、接している地雷の数などの情報を格納するためのデータフレーム。接している地雷の数が1,2,3,4,...の場合dfには、対応する地雷の数が代入されます。また地雷は-1として表現されます。
exploredTile
探索済みの盤面を代入します。TRUEはすでに探索済みのセルであり、FALSEの場合は探索していないセルを表現します。TRUEの盤面のみがGUI上で描画されます。
flaggedTile
旗付きのセルを格納するデータフレーム。TRUEの場合、旗が付いていて、FALSEの場合、旗がついていないことを表現します。後述するmodeがflagのときにクリックしたセルに対応するflaggedTileがTRUEとなります。
gameEnd
ゲームが続いているか、終了しているかを表すlogical型の変数。
mode
探索するか、旗を付けるかを表すcharacter型の変数。modeがexploreの場合、選択したセルを探索します(セルを開ける)。flagの場合、選択したセルに旗を付けます。
remainingCells
未探索のセルの数を表すnumeric型の変数。
メソッド
Initialize
Initializeは、フィールドの変数を初期化するためのメソッドです。
インスタンスの生成後に、このメソッドを実行することで、フィールドの変数を初期化できます。
gameEnd、mode、remainigCells、df、eploredTile、flaggedTileなどの変数を初期化しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | Initialize = function(){ gameEnd <<- FALSE mode <<- "explore" remainingCells <<- n_row * n_col - n_bomb df <<- data.frame(matrix(rep(NA, n_row), nrow = n_row))[,numeric(0)] for(i in 1:n_col){ df <<- cbind(df, rep(0,n_row)) } exploredTile <<- data.frame(matrix(rep(F, n_row), nrow = n_row))[,numeric(0)] for(i in 1:n_col){ exploredTile <<- cbind(exploredTile, rep(F,n_row)) } flaggedTile <<- data.frame(matrix(rep(F, n_row), nrow = n_row))[,numeric(0)] for(i in 1:n_col){ flaggedTile <<- cbind( flaggedTile, rep(F,n_row)) } } |
Start
Initializeで初期化したdfに盤面の情報を与えるメソッドです。
dfに地雷の数n_bombの数だけ、-1を代入し地雷の情報を与えます。
そのあとはdfの-1の位置を頼りに、-1に接している数をdfの各要素に代入していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | Start = function(){ xy_bomb <- NULL for(i in 1:n_bomb){ xy <- sample(setdiff(1:(n_row*n_col), xy_bomb),1) xy_bomb[i] <- xy df[(xy%/%n_col)+1,(xy%%n_row)+1] <<- (-1) } for(i in 1:n_row){ for(j in 1:n_col){ if(df[i,j]!=-1){ count <- 0 if(i < n_row){ if(df[i+1,j] == -1){count <- count + 1} } if(i > 1){ if(df[i-1,j] == -1){count <- count + 1} } if(j < n_col){ if(df[i,j+1] == -1){count <- count + 1} } if(j > 1){ if(df[i,j-1] == -1){count <- count + 1} } if(i < n_row & j < n_col){ if(df[i+1,j+1] == -1){count <- count + 1} } if(i < n_row & j > 1){ if(df[i+1,j-1] == -1){count <- count + 1} } if(i > 1 & j < n_col){ if(df[i-1,j+1] == -1){count <- count + 1} } if(i > 1 & j > 1){ if(df[i-1,j-1] == -1){count <- count + 1} } df[i,j] <<- count } } } } |
Explore
マウスでクリックした座標をもとに、dfを探索していくメソッドです。
マインスイーパーの探索法と同様に、指定された座標x,yから再帰的に探索していきます。
1,2, . . . , 8まで探索し、探索後の座標はexploredTileの対応する座標にTRUEを代入し、return()を返し、探索を終了します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | Explore = function(x,y){ if(x <= n_row & x >= 1 & y <= n_col & y >= 1){ if(mode == "explore"){ if(df[x,y] == -1 & exploredTile[x,y] == F){ exploredTile[x,y] <<- T gameEnd <<- TRUE return() }else if(df[x,y] >= 1 & exploredTile[x,y] == F){ exploredTile[x,y] <<- T return() }else if(df[x,y] == 0 & exploredTile[x,y] == F){ if(x < n_row){ exploredTile[x,y] <<- T Explore(x+1,y) } if(x > 1){ exploredTile[x,y] <<- T Explore(x-1,y) } if(y < n_col){ exploredTile[x,y] <<- T Explore(x,y+1) } if(y > 1){ exploredTile[x,y] <<- T Explore(x,y-1) } if(x < n_row & y < n_col){ exploredTile[x,y] <<- T Explore(x+1,y+1) } if(x < n_row & y > 1){ exploredTile[x,y] <<- T Explore(x+1,y-1) } if(x > 1 & y < n_col){ exploredTile[x,y] <<- T Explore(x-1,y+1) } if(x > 1 & y > 1){ exploredTile[x,y] <<- T Explore(x-1,y-1) } } }else{ if(flaggedTile[x,y] == F){ flaggedTile[x,y] <<- T return() } } } } |
GameStartPlot
マインスイーパーを開始したときのゲーム画面をplotで描画します。
マインスイーパーの盤面の格子や枠、exploreボタンやflagボタンの描画などをゲーム開始時に行います。
ゲームを開始した後はGameStartPlotではなく、次で紹介するGamePlotでゲームを描画していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | GameStartPlot = function(){ windows(n_col,n_row) plot.new() for(i in 1:n_row){ for(j in 1: n_col){ rect((j-1)/n_col , (1 - i/(n_row + 1)) , j/n_col, ( 1 + (1-i)/(n_row + 1)) , col = "grey", pin = c(n_col, (n_row + 1)), lty = 2) } } rect(0 , (1 - n_row/(n_row + 1)) , 1, 1 , pin = c(n_col, (n_row + 1))) rect(0.38, 0, 0.48, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81)) text(list(x=0.43 ,y=1/(2*(n_row + 1))), labels = "explore") rect(0.52, 0, 0.62, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81, alpha = 0.5)) text(list(x=0.57 ,y=1/(2*(n_row + 1))), labels = "flag") text(list(x=0.2 ,y=1/(2*(n_row + 1))), labels = paste0("remain:", remainingCells)) text(list(x=0.8 ,y=1/(2*(n_row + 1))), labels = paste0("bombs:", n_bomb)) } |
GamePlot
GamePlotは、マインスイーパーを実行中に、セルの選択やexploreやflagボタンを押した後に実行されるメソッドです。
盤面の情報が更新された後に、このメソッドを用いてGUI上の描画も更新していきます。
また画像ファイルを読み込み、ゲーム終了時のGame ClearやGame overなどの描画を行ったり、地雷の画像や旗の画像などを描画したりしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | GamePlot = function(){ library(png) pass <- getwd() setwd(paste0(pass, "/", "Picture")) bombImage <- readPNG("bomb.png") flagImage <- readPNG("flag.png") gameOverImage <- readPNG("gameover.png") gameClearImage <- readPNG("gameclear.png") setwd(pass) for(i in 1:n_row){ for(j in 1:n_col){ if(df[i,j] == 0 & exploredTile[i,j] == T){ rect((j-1)/n_col , (1 - i/(n_row + 1)),j/n_col, ( 1 + (1-i)/(n_row + 1)), col = "white", pin = c(n_col, (n_row+1))) } else if(df[i,j] >= 1 & exploredTile[i,j] == T){ rect((j-1)/n_col , (1 - i/(n_row + 1)),j/n_col, ( 1 + (1-i)/(n_row + 1)), col = "white", pin = c(n_col, (n_row + 1))) text(list(x=(2*j-1)/(2*n_col) ,y=(1 + (1-2*i)/(2*(n_row + 1)))), labels = as.character(df[i,j])) } else if(df[i,j] == -1 & exploredTile[i,j] == T){ if(mode == "explore"){ for(m in 1:n_row){ for(n in 1:n_col){ if(df[m,n] == -1){ rect((n-1)/n_col , (1 - m/(n_row + 1)), n/n_col, ( 1 + (1-m)/(n_row + 1)), col = "white", pin = c(n_col, (n_row + 1))) rasterImage(bombImage, (n-1)/n_col , (1 - m/(n_row + 1)),n/n_col, ( 1 + (1-m)/(n_row + 1))) } } } } }else if(flaggedTile[i,j] == T){ rect((j-1)/n_col , (1 - i/(n_row + 1)), j/n_col, ( 1 + (1-i)/(n_row + 1)), col = "white", pin = c(n_col, (n_row + 1))) rasterImage(flagImage, (j-1)/n_col , (1 - i/(n_row + 1)),j/n_col, ( 1 + (1-i)/(n_row + 1))) } } } rect(0.1, 0, 0.3, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white", lty = 0) text(list(x=0.2 ,y=1/(2*(n_row + 1))), labels = paste0("remain:", remainingCells)) rect(0.7, 0, 0.9, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white", lty = 0) text(list(x=0.8 ,y=1/(2*(n_row + 1))), labels = paste0("bombs:", n_bomb)) if(mode == "explore"){ rect(0.38, 0, 0.48, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white") rect(0.38, 0, 0.48, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81)) text(list(x=0.43 ,y=1/(2*(n_row + 1))), labels = "explore") rect(0.52, 0, 0.62, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white") rect(0.52, 0, 0.62, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81, alpha = 0.5)) text(list(x=0.57 ,y=1/(2*(n_row + 1))), labels = "flag") }else if(mode == "flag"){ rect(0.38, 0, 0.48, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white") rect(0.38, 0, 0.48, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81, alpha = 0.5)) text(list(x=0.43 ,y=1/(2*(n_row + 1))), labels = "explore") rect(0.52, 0, 0.62, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = "white") rect(0.52, 0, 0.62, 1/(n_row + 1) - 1/(10*(n_row + 1)), col = rgb(0.25, 0.95, 0.81)) text(list(x=0.57 ,y=1/(2*(n_row + 1))), labels = "flag") } if(gameEnd == TRUE & remainingCells == 0){ rasterImage(gameClearImage, 0, 0, 1, 1) }else if(gameEnd == TRUE & remainingCells > 0){ rasterImage(gameOverImage, 0, 0, 1, 1) } } |
MousePointer
マウスの左クリックの座標を取得し、GUI上の[0, 1]の値をとる座標を対応する盤面の(x, y)座標に変換するメソッドです。
例えばGUI上では、x軸は0から1の値をとるように設定されています。フィールド中のdfやexploredTileの対応するx座標を得たい場合、x軸を、1から列の数に変換する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | MousePointer = function(x,y){ x0 <- 0 y0 <- 0 for(i in 1:(n_row+1)){ if((1 - i/(n_row + 1)) <= y & y <= (1 + (1-i)/(n_row + 1))){ y0 <- i } } for(i in 1:n_col){ if((i-1)/n_col <= x & x <= i/n_col){ x0 <- i } } return(list(y0, x0)) } |
ConvertMode
メソッドConvertModeは、ゲームモードをexploreやflagに切り替えます。
GUI上で左クリックした座標が、exploreボタンやflagボタンであった場合、そのボタンに対応するゲームモードに切り替えます。
例えば、flagボタンがクリックされた場合、ゲームモードはflagに切り替わり、盤面に旗を設置できるようになります。
1 2 3 4 5 6 7 8 9 10 | ConvertMode = function(x, y){ if(y < ( 1 + (1-n_row)/(n_row + 1))){ if(x >= 0.38 & x <= 0.48 & y <= 1/(n_row + 1) - 1/(10*(n_row + 1))){ mode <<- "explore" } if(x >= 0.52 & x <= 0.62 & y <= 1/(n_row + 1) - 1/(10*(n_row + 1))){ mode <<- "flag" } } } |
RemainingCells
メソッドRemainingCellsは、盤面に残っている地雷以外のセルの数を求めます。
メソッドGamePlotで、左下の残りのセル数を描画する際に、このメソッドで求めたセルの数が用いられます。
1 2 3 4 5 6 7 | RemainingCells = function(){ tileVec <- as.vector(exploredTile) remainingCells <<- (n_row * n_col - n_bomb) - length(tileVec[tileVec == TRUE]) if(remainingCells== 0){ gameEnd <<- TRUE } } |
ゲームの開始方法
main()
ゲームを開始するには以下の関数main()を実行します。コンソール上でmain()と入力することで、ゲームを開始することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | main <- function(){ restart <- TRUE while(restart == TRUE){ numberOfRows <- NULL numberOfCols <- NULL numberOfBombs <- NULL while(is.null(numberOfRows) | !is.numeric(as.numeric(numberOfRows))){ numberOfRows <- readline("The number of rows:") if(is.na(as.numeric(numberOfRows))){ numberOfRows <- NULL } } while(is.null(numberOfCols) | !is.numeric(as.numeric(numberOfCols))){ numberOfCols <- readline("The number of cols:") if(is.na(as.numeric(numberOfCols))){ numberOfCols <- NULL } } while(is.null(numberOfBombs) | !is.numeric(as.numeric(numberOfBombs))){ numberOfBombs <- readline("The number of bombs:") if(is.na(as.numeric(numberOfBombs))){ numberOfBombs <- NULL } } ms <- new("mineSweeper", n_row = as.numeric(numberOfRows), n_col = as.numeric(numberOfCols), n_bomb = as.numeric(numberOfBombs)) ms$Initialize() ms$Start() ms$GameStartPlot() while(ms$gameEnd == FALSE){ options(locatorBell = FALSE) position <- locator(1) set <- ms$MousePointer(position$x, position$y)[] ms$Explore(set[[1]], set[[2]]) ms$ConvertMode(position[1]$x, position[2]$y) ms$RemainingCells() ms$GamePlot() } continue <- NULL while(is.null(continue) | !is.numeric(as.numeric(continue))){ continue <- readline("Do you wanto to Restart? Yes:1 No:0 ") if(is.na(as.numeric(continue))){ continue <- NULL } } if(continue == 0){restart <- FALSE} } } |
main()を実行すると、マインスイーパーの行、列、地雷の数などをreadline()で入力した後に、mineSweeperクラスのインスタンスを生成し、各メソッドを動的に用いてゲームのシーンを描画しています。
ゲームの実行方法
マインスイーパーを実行するために、スクリプトファイル中のクラスmineSweeperと関数main()を読み込みます。
読み込んだ後は、コンソール上で次のようにmain()と実行しましょう。
1 | > main() |
実行すると、盤面の行数、列数、地雷の数を入力を求められます。好きな数字を入力しエンターを押しましょう。
1 2 3 | The number of rows:10 The number of cols:10 The number of bombs:10 |
エンターを入力すると、次の画像のようなwindowが起動します。
盤面をクリックすることで、マインスイーパーを遊ぶことができます。
また、flagボタンがを押すことで、旗を置くことができます。flagを押した後はexploreボタンを押すと、探索モード(通常のモード)に切り替わります。
ゲームをクリアした後やゲームオーバーのとき、次の画像のようにwindow上にGame ClearやGame Overが表示された後に、コンソール上で"Do you wanto to Restart? Yes:1 No:0 "とゲームをリスタートするか質問されます。
リスタートしたいときは1を、終了したいときは0を押してください。
マインスイーパーの実行方法は以上となります。
ダウンロード方法
今回、紹介したマインスイーパーは以下のダウンロードリンクから入手できます。
R言語 MineSweeper Rスクリプト
スクリプトファイル以外にもゲームで用いられるpngファイル等が含まれています。
是非是非ダウンロードしてください。
R言語でゲームを作成する際の参考にしてくれたら幸いです。