
ローグっぽい2D迷路探索ゲームを作る1(PowerShellで)
はじめに
Windowsに標準搭載されているPowerShellを使って
ローグっぽい2D迷路探索ゲーム開発に挑戦します。
グラフィックは文字だけのこのようなゲームです。

^ が自分。最初は北を向いています。
周り3ブロックだけ見えていて
1歩ずづコマンドで進んでゴールを目指します。
しょぼすぎ
今回は最初なので迷路の中を歩くだけにしてます。
3回くらいの記事にしようと思います。
1回目 ローグの解説、ゲーム基礎部分
2回目 ゲーム部分の機能強化
3回目 ランダムな迷路の自動生成
今回は1回目の記事です。
ローグとは?
1980年代に開発されたゲーム『ローグ』を起源とするマップ探索ゲームです。ランダム生成されるダンジョンを探索し、パーマデス(キャラクターが死亡するとゲームオーバー)のシステムを採用していることが大きな特徴です。80年代以降このローグを起源とする派生ゲームが多く作られました。ローグライクゲームと呼びます。
ローグの特徴
ランダム生成ダンジョン: 毎回プレイする度に異なる構造のダンジョンが生成され、探索のたびに新しい体験が得られます。
パーマデス: キャラクターが死亡すると、そのゲームデータは失われ、最初からやり直しとなります。
ターン制の戦闘: 行動がターン制で行われ、戦略的な戦闘が求められます。
豊富なアイテム: ダンジョン内で様々なアイテムを入手し、キャラクターを強化することができます。
文字ベースのUI: 古典的なローグライクゲームでは、文字で情報が表現されることが多く、想像力を刺激する要素がありました。
2D迷路探索ゲームとの関係
2D迷路探索ゲームは、その構造上、ローグライクゲームとの親和性が高く、多くの2D迷路探索ゲームがローグライク要素を取り入れています。ランダム生成された迷路を探索し、様々なアイテムを入手しながらゴールを目指すという基本的なゲームシステムは、ローグライクゲームの典型的な要素と言えるでしょう。
ローグライクゲームの例
不思議のダンジョンシリーズ: 日本を代表するローグライクゲームシリーズ。
Binding of Isaac: 非常に高い人気を誇るローグライクゲーム。
Dead Cells: メトロイドヴァニア要素を取り入れたローグライクゲーム。
ローグライクゲームの魅力?
ローグライクゲームの魅力は、一言で言い表すのは難しいほど多岐にわたります。その魅力をいくつかご紹介します。
毎回新しい体験: ランダム生成されるダンジョンは、毎回異なるレイアウト、敵、アイテム配置となり、同じゲームを何度もプレイしても飽きることがありません。まるで新しいゲームをプレイしているような新鮮な体験が味わえます。
高まる緊張感と達成感: パーマデス(死亡するとゲームオーバー)のシステムは、一歩一歩が緊張感に満ち溢れています。しかし、困難を乗り越え、新たな階層に到達したり、強力な敵を倒したりしたときの達成感は格別です。
戦略性の高い戦闘: ターン制の戦闘が多く、敵の行動パターンを予測し、適切なスキルやアイテムを選択するなど、戦略的なプレイが求められます。
キャラクターの成長: 獲得したアイテムや経験値でキャラクターを強化していく過程は、RPGのような楽しさがあります。
やり込み要素の豊富さ: 新しいキャラクター、難易度の高いモード、隠し要素など、やり込み要素が豊富で、長く楽しめるゲームが多いです。
ローグライクゲームの魅力は、人によって感じ方が異なります。
探検好きな人: ランダム生成されるダンジョンを探索し、未知の世界を探検する楽しさを味わえます。
戦略ゲーム好き: ターン制の戦闘やアイテムの組み合わせなど、戦略的なプレイが好きな人におすすめです。
高難易度ゲーム好き: パーマデスのシステムは、高い難易度を求める人にとって魅力的です。
RPG好き: キャラクターの成長やアイテム収集など、RPG要素を楽しめます。
これらの要素が組み合わさることで、ローグライクゲームは、中毒性が高く、何度もプレイしたくなる独特の魅力を持っていると言えるでしょう。
開発
環境
WindowsとPowerShell v5.1系で動作を確認しています。
PS C:\temp> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.22621.4249
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.22621.4249
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
最初のプログラム
$maze=@(
"***********",
"* * *",
"* * *** * *",
"* ** * *",
"* *** *",
"* *** *G* *",
"* * * * *",
"* * * * * *",
"*S* * *",
"***********"
)
# 変数初期化
# 自分の位置と向き
$my_x = -1
$my_y = -1
$my_direction = 0 # 向き 0北 1東 2南 3西
$my_char = @("^",">","v","<") # 向き 0北 1東 2南 3西
$my_scope = 3 # 見える範囲
# ゴールの位置
$goal_x = -1
$goal_y = -1
# 移動用変数
$dx = @(0,1,0,-1)
$dy = @(-1,0,1,0)
# スタートとゴールを探す
for($i = 0;$i -lt $maze.length;$i++) {
for($j = 0;$j -lt $maze[$i].length;$j++) {
if($maze[$i][$j] -eq "S") {
$my_x = $j
$my_y = $i
}
if($maze[$i][$j] -eq "G") {
$goal_x = $j
$goal_y = $i
}
}
}
# 自分キャラの上書き
$maze[$my_y] = $maze[$my_y].Remove($my_x,1)
$maze[$my_y] = $maze[$my_y].Insert($my_x,$my_char[$my_direction])
function disp-maze($x,$y,$maze,$scope) {
# 表示範囲の計算
$mx = $x - $scope
$my = $y - $scope
if($mx -lt 0) {$mx = 0}
if($my -lt 0) {$my = 0}
# 表示範囲の計算 横の最大値
$width = $x + $scope
if($width -ge $maze[0].length) {
$width = $maze[0].length - 1
}
# 表示範囲の計算 縦の最大値
$height = $y + $scope + 1
if($height -gt $maze.length) {
$height = $maze.length
}
# 枠の表示 上側
write-host "+" -NoNewline
for($i=0;$i -le $width-$mx+2;$i++){
write-host "-" -NoNewline
}
write-host "+"
# 自分の周りの表示
for($yy = $my;$yy -lt $height;$yy++) {
$temp_maze = ""
for($xx=$mx;$xx -le $width;$xx++) {
$temp_maze+=$maze[$yy][$xx]
}
write-host "| $temp_maze |"
}
# 枠の表示 下側
write-host "+" -NoNewline
for($i=0;$i -le $width-$mx+2;$i++){
write-host "-" -NoNewline
}
write-host "+"
}
$endflg = $True
while($endflg) {
# 画面表示
disp-maze $my_x $my_y $maze $my_scope
# 入力
$keyinput = [int](read-host "どちらに進みますか 0:北 1:東 2:南 3:西 99:終了")
# 入力チェック
if($keyinput -ne "0" -and $keyinput -ne "1" -and$keyinput -ne "2" -and $keyinput -ne "3" -and $keyinput -ne "99") {
# 該当の数字以外は無効とする
continue;
}
# 終了判定
if($keyinput -eq "99"){
$endflg = $False
continue;
}
# 移動処理
if($keyinput -ne $my_direction) {
# 向きが違う場合、向きを変えるのみ
$tmp_my_direction = $keyinput
$tmp_my_x = $my_x
$tmp_my_y = $my_y
} else {
# 向きが同じなら進む
$tmp_my_direction = $keyinput
$tmp_my_x = $my_x+$dx[$tmp_my_direction]
$tmp_my_y = $my_y+$dy[$tmp_my_direction]
}
# 進む処理
if($maze[$tmp_my_y][$tmp_my_x] -ne "*"){# 壁の判定
# ゴールの判定
if($maze[$tmp_my_y][$tmp_my_x] -eq "G"){
write-host "ゴールに到着しました"
$endflg = $False
}
# 現在地を空白で消す
$maze[$my_y] = $maze[$my_y].Remove($my_x,1)
$maze[$my_y] = $maze[$my_y].Insert($my_x," ")
# 位置移動
$my_x = $tmp_my_x
$my_y = $tmp_my_y
$my_direction = $tmp_my_direction
# 表示
$maze[$my_y] = $maze[$my_y].Remove($my_x,1)
$maze[$my_y] = $maze[$my_y].Insert($my_x,$my_char[$my_direction])
}
}
遊び方は
WindowsボタンとRキー同時に押します
出てきた画面に半角小文字でpowershellと入力してOKボタンを押します

黒っぽい画面が出てきたら、
↑ のプログラムをコピーしてターミナルに張り付けてください

「強制的に貼り付け」をクリック

張り付けたら、エンターキーを押す。
これで遊べます
プログラムの解説
前半部分 初期化:
変数を初期化して、迷路データから自分の位置と、ゴールの位置を探しています。
自分の向きとキャラクター表示は配列変数を使って表現しています。
$my_direction = 0 #向き 0北 1東 2南 3西
$my_char = @("^",">","v","<") #向き 0北 1東 2南 3西
ぱっと見て分かりにくいと思うのですが、
$my_char[$my_direction]
このようにして自分の向きに合わせてキャラクターが変わるようにしています。
向きの0,1,2,3はテンキーに合わせたほうが良かったかも。
わかりにくいのはここ
# 自分キャラの上書き
$maze[$my_y]=$maze[$my_y].Remove($my_x,1)
$maze[$my_y]=$maze[$my_y].Insert($my_x,$my_char[$my_direction])
迷路データ上のSを自分キャラに書き換えています。
Replaceで書き換えてもいいんですが、後半の移動処理の部分と書き方を合わせるためにこのようにしました。
関数disp-maze
自分の周りだけを表示しています。
自分のx,y座標の周り$scope値だけを$mazeから切り抜いて表示する処理です。切り出す範囲を引数の$scopeにしたのは後で変更できるようにです。
後半部分 while($endflg) :
while($endflg) {~}
ここでコマンド入力と移動を行っています。
仮の座標変数$tmp_my_x、$tmp_my_yに進む方向の座標位置を入れて
そこが壁以外だったら進める、進んだら画面表示する
という処理をしています。
配列変数$dx、$dyは移動方向の計算用。
$dx=@(0,1,0,-1)、$dy=@(-1,0,1,0)
このように定義されていて
向きの変数$my_directionの値が 0:北 1:東 2:南 3:西 になっているので
$dx[$my_direction]
$dy[$my_direction]
とすることでxy座標の移動値を出すことができます。
$my_directionの値が 2の場合
$dx[2] ... 0
$dy[2] ... 1
なので
$x = $x + $dx[2]
$y = $y + $dy[2]
と計算すると南方向へ座標計算できます。こうするとCASE文やIF文で書くよりスッキリします。競技プログラミングでお馴染みのテクニック。
次回予告
もう少しゲーム性を出すために改良します
・食料の概念追加
一歩ごとに食料が減っていき0になると終了
迷路上で食料が取れる
・イベント追加
ときどき何か起こる
・壁を壊す
1度だけ壁が壊せる
・向きをテンキーに合わせる