46
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

250行のVue.jsで陣取りゲームを作った

Last updated at Posted at 2019-09-29

こんにちは、猫チーズです。

jintori.gif

黒色:ユーザー
灰色:自陣
赤色:敵
白色:敵陣
茶色:縄(自陣を広げるためのもの)

前回書いた記事『200行のVue.jsでスネークゲームを作った』が中々反響良さそうでしたので、Vue.js勉強用ゲーム 第2弾として陣取りゲームを作ってみました。

十数年前に何かのゲーム機で遊んだこのゲームを、うろ覚えで再現しています。
もし本家を知っている方いましたら教えてください!

デモページ

GitHub ソースコード

ゲームルール

  1. 自陣の中では矢印キーで自由に動き回れる
  2. 敵陣に入ると、縄を張りながらまっすぐ進む
  3. 縄を張ったまま自陣に戻ると、自陣が増える
  4. 敵が縄にぶつかるとゲームオーバー
  5. 自陣を増やすとスコアUP

これらのルールをVue.jsで作りました。

250行のプログラム

以下の250行のhtmlファイルに全ての機能が纏まっています。
jintori.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

46
55
2

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
46
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?