こんにちは、猫チーズです。
黒色:ユーザー
灰色:自陣
赤色:敵
白色:敵陣
茶色:縄(自陣を広げるためのもの)
前回書いた記事『200行のVue.jsでスネークゲームを作った』が中々反響良さそうでしたので、Vue.js勉強用ゲーム 第2弾として陣取りゲームを作ってみました。
十数年前に何かのゲーム機で遊んだこのゲームを、うろ覚えで再現しています。
もし本家を知っている方いましたら教えてください!
デモページ
GitHub ソースコード
ゲームルール
- 自陣の中では矢印キーで自由に動き回れる
- 敵陣に入ると、縄を張りながらまっすぐ進む
- 縄を張ったまま自陣に戻ると、自陣が増える
- 敵が縄にぶつかるとゲームオーバー
- 自陣を増やすとスコアUP
これらのルールをVue.jsで作りました。
250行のプログラム
以下の250行のhtmlファイルに全ての機能が纏まっています。
jintori.htmlなどの名前で保存して、ブラウザでそのファイルを開くと遊べます。
機能の改造などをして遊んでみてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>陣取りゲーム</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<style>
/* グリッドレイアウト */
#grid {
display: grid;
grid-template-columns: repeat(20, 20px); /* 横20マス 幅20px */
grid-template-rows: repeat(20, 20px); /* 縦20マス 高さ20px */
}
/* セルの色 */
.cell {
border: 1px solid white;
background: whitesmoke;
}
/* ユーザーの陣地の色 */
.cell.area {
background: darkgray;
}
/* ロープの色 */
.cell.lope {
background: burlywood;
}
/* ユーザーの色 */
.cell.user {
background: black;
}
/* 敵の色 */
.cell.enemy {
background: orangered;
}
</style>
</head>
<body>
<div id='app'>
<p>SCORE: {{ score }}</p>
<div id='grid'>
<!-- セルを400個生成して、必要に応じてuser, area, lope, enemyクラスを付ける -->
<!-- (注意:Vueは v-for="i in 数値" としたとき、iが1から始まる) -->
<div v-for="i in grid.width * grid.height"
:class="{
cell: true,
area: user.area_indexes.includes(i - 1),
lope: user.lope_indexes.includes(i - 1),
user: user_index === i - 1,
enemy: enemy_index === i - 1,
}"
><!--{{ i - 1 }}--></div>
</div>
<p v-if='is_gameover'>
GAME OVER<br>
<button onclick="location.reload()">RETRY</button>
</p>
</div>
<script>
new Vue({
el: '#app',
data: {
speed: 200, // 1マス進むのにかかる時間[ms]
// グリッドのデータ
grid: {
width: 20, // 横20マス
height: 20, // 縦20マス
},
// ユーザーのデータ
user: {
pos: { x: 0, y: 0 }, // 位置座標
direction: { dx: 1, dy: 0 }, // 進行方向(初期値は右)
area_indexes: [], // ユーザーの陣地(インデックスの配列)
lope_indexes: [], // ロープ(インデックスの配列)
},
// 敵のデータ
enemy: {
pos: { x: 10, y: 10 }, // 位置座標
direction: { dx: -1, dy: 1 }, // 進行方向(初期値は左下)
},
},
// 初期化
created() {
// 上下左右端をユーザーのエリアにする
for(let x = 0; x < this.grid.width; x++) {
for(let y = 0; y < this.grid.height; y++) {
const is_side = x === 0 || x === this.grid.width - 1 || y === 0 || y === this.grid.height - 1
if(is_side) {
this.user.area_indexes.push(this.pos2index({x, y}))
}
}
}
// キーボード入力のイベントをon_keydownメソッドに投げる
document.onkeydown = () => {
this.on_keydown(event.keyCode)
}
// 時間を動かし始める
this.time_goes()
},
computed: {
// スコア = ユーザーのエリアの量(初期の領域を除く)
score() {
return this.user.area_indexes.length - this.grid.width * 2 - this.grid.height * 2 + 4
},
// ユーザーの座標をインデックスに変換
user_index() {
return this.pos2index(this.user.pos)
},
// 敵の座標をインデックスに変換
enemy_index() {
return this.pos2index(this.enemy.pos)
},
// ゲームオーバーの条件
is_gameover() {
return this.user.lope_indexes.includes(this.enemy_index)
},
},
methods: {
// 時間を進める
time_goes() {
if(this.is_gameover) return
this.forward_user()
this.forward_enemy()
setTimeout(this.time_goes.bind(this), this.speed) // speedミリ秒後に自分自身を呼び出す
},
// 受け取った座標をインデックスに変換する
pos2index({x, y}) {
return this.grid.width * y + x
},
// ユーザーを進める
forward_user() {
const old_pos = { ...this.user.pos }
// directionの分だけ移動させる
this.user.pos.x += this.user.direction.dx
this.user.pos.y += this.user.direction.dy
const pos_owner = this.get_cell_owner(this.user.pos) // 現在地の所有者
// 現在地が場外なら、位置座標を巻き戻す
if(pos_owner === 'frameout') {
this.user.pos = old_pos
return
}
// 現在地が敵陣の中なら、ここにロープを追加
if(pos_owner === 'enemy') {
this.user.lope_indexes.push(this.user_index)
}
// ロープを伸ばしたままユーザーの陣地まで辿り着いた場合
if(pos_owner === 'user' && this.user.lope_indexes.length) {
const enemy_area = this.get_enemy_area()
// 敵エリア以外のエリアをユーザーのエリアにする
this.user.area_indexes = []
this.user.lope_indexes = []
for(let x = 0; x < this.grid.width; x++) {
for(let y = 0; y < this.grid.height; y++) {
// このセル(x, y)が敵の陣地か否か
const is_enemy_area = enemy_area.left <= x && x <= enemy_area.right && enemy_area.top <= y && y <= enemy_area.bottom
if(is_enemy_area) continue
this.user.area_indexes.push(this.pos2index({x, y}))
}
}
}
},
// 敵を進める
forward_enemy() {
const { pos, direction } = this.enemy
// ユーザーのエリアにぶつかるなら跳ね返す
const is_collided_x = this.get_cell_owner({ x: pos.x + direction.dx, y: pos.y }) === 'user'
const is_collided_y = this.get_cell_owner({ x: pos.x, y: pos.y + direction.dy }) === 'user'
if(is_collided_x) direction.dx *= -1 // 進行方向を左右反転
if(is_collided_y) direction.dy *= -1 // 進行方向を上下反転
// directionの分だけ移動させる
pos.x += direction.dx
pos.y += direction.dy
},
// 指定したセルの所有者を取得する
get_cell_owner({x, y}) {
if(x < 0 || this.grid.width <= x || y < 0 || this.grid.height <= y) return 'frameout'
if(this.user.area_indexes.includes(this.pos2index({x, y}))) return 'user'
if(this.user.lope_indexes.includes(this.pos2index({x, y}))) return 'lope'
return 'enemy'
},
// 敵陣地の範囲(left, top, right, bottom)を取得する
get_enemy_area() {
const { x, y } = this.enemy.pos
const area = { left: x, top: y, right: x, bottom: y } // 初期値 = 敵自身の座標
// 上下左右それぞれの方向で敵陣地の範囲を調べる
while(this.get_cell_owner({x: area.left - 1, y}) === 'enemy') area.left--
while(this.get_cell_owner({x: area.right + 1, y}) === 'enemy') area.right++
while(this.get_cell_owner({x, y: area.top - 1}) === 'enemy') area.top--
while(this.get_cell_owner({x, y: area.bottom + 1}) === 'enemy') area.bottom++
return area
},
// キー入力を受け取ってユーザーの進行方向を変える
on_keydown(keyCode) {
if(this.get_cell_owner(this.user.pos) !== 'user') return // ユーザーのエリア外では方向転換できない
switch(keyCode) {
case 37: this.user.direction = {dx: -1, dy: 0}; break // 「←」キーが押された
case 38: this.user.direction = {dx: 0, dy: -1}; break // 「↑」キーが押された
case 39: this.user.direction = {dx: 1, dy: 0}; break // 「→」キーが押された
case 40: this.user.direction = {dx: 0, dy: 1}; break // 「↓」キーが押された
}
},
},
})
</script>
</body>
</html>
以下の機能は私が今思いついたものですが、よければ機能追加してみてください。難易度順です。
・スピードを速くする
・配色を変える
・マップのサイズを変える
・経過時間を表示
・敵の出現位置をランダムにする
・敵を増やす
猫チーズ
高校一年の頃からアプリ作りに没頭していて、今はアプリクリエイターとして生きるために奮闘中です。
今年の4月からサイバーブレイン株式会社でデザイナーを始めました。
Hello 猫チーズ | 猫チーズと擬似会話ができるブログ
https://blog.miyauchi-akira.app/post/20190927/
Twitter | 猫チーズ
https://twitter.com/miyauchoi
ポートフォリオ | ミヤウチアキラ
https://miyauchi-akira.app