はじめに
2020年も残すところ数時間となりました。みなさま、いかがお過ごしでしょうか。
筆者はリアルが寂しい人間なので、今日も今日とて家で引きこもっております。
今年はCOVID-19の影響で同じように家で引きこもっている人が多いかと思いまして、そんな皆さんでも楽しめるようなオセロを作成しました。
前置きはこれくらいにして本題に入ると、vue.jsを使って6×6のオセロのwebアプリを作りました。まだまだ勉強中の身なので、おかしな点があったら気軽に指摘していただけると嬉しいです。
公開URL:https://eycjur.github.io/pages/othello.html
ソース:https://github.com/eycjur/pages/blob/main/public/othello.html
プログラム
基本的なコードは以下の通りです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>66othello</title>
<style>
#app{
text-align: center;
}
</style>
</head>
<body>
<div id="app">
<!-- 盤面 -->
<!-- pass -->
<div id="text">
<p id="message"></p>
</div>
</div>
<script src="js/vue.js"></script>
<script>
let app = new Vue({
el: "#app",
data: {
pass;
},
methods:{
function: function(){
pass;
},
},
created() {
pass;
}
});
</script>
</body>
</html>
これからどんどん肉付けしていきます。
まず、オセロを作るためには、石と盤を準備する必要があります。
盤面は以下のような配列にします。
this.cells = [
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0,-1, 1, 0, 0],
[0, 0, 1,-1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
];
これを表示する際には以下のように表にして表示します。
<!-- vueで表を作る -->
<table border="1" cellspacing="0">
<tr v-for="cell in cells">
<td v-for="i in cell" class="cell" v-on:click="clicked()">
<span v-if="i===1">⚫</span>
<span v-if="i===-1">⚪</span>
<span v-if="i===0"></span>
</td>
</tr>
</table>
⚫``⚪
は絵文字でそれぞれ黒石、白石を表します。また、v-on:click="clicked()"
とすることで、それぞれのマスがクリックされたときの処理を書いています。
clicked()
関数では、押されたマスの行と列の位置を取り出します。
// クリック時の処理
clicked: function(ev){
// イベントから行と列を特定
ev = ev || window.event;
elem = ev.target;
let list_tr = document.getElementsByTagName("tr");
let list_td = elem.parentNode.childNodes;
list_tr = [].slice.call(list_tr);
list_td = [].slice.call(list_td);
tr_num = list_tr.indexOf(elem.parentNode);
td_num = list_td.indexOf(elem);
this.put(tr_num, td_num)
},
ev.target
でイベントが発生した要素を、elem.parentNode
でその親要素を取得します。これらに対してlist_td
,list_tr
でそれと並列にあるすべての要素を持つ配列にすることで、indexOf()
によって押されたセルの番号がとれるようにしています。
これらの番号を用いて、this.put()
関数(後述)で石を置く処理をします。
ここでは、石を置く処理の前におけるかどうか判定する関数を記述しておきます。
まず、石を返せるかどう確認する方法としては、押されたセルから周囲の8方向に対して各方向に(石がないセルを挟まずに)自分の石がある方向かを判定します。もしあれば、その間にある相手の石の位置に返せるというフラグthis.flag_cells
を立てます。
まず、8方向を定義しておきます。
direction: [
[-1, -1],
[0, -1],
[1, -1],
[1, 0],
[1, 1],
[0, 1],
[-1, 1],
[-1, 0],
],
本題の関数を記述します。
// 返せるか確認
turn_over: function(tr_num, td_num){
this.direction.forEach(dir => {
let td_here = td_num + dir[0];
let tr_here = tr_num + dir[1];
// 自分の石があるか確認
let flag_mystone = false;
while (this.check_end(tr_here, td_here)){
if (this.cells[tr_here][td_here] === 0-this.turn){
td_here += dir[0];
tr_here += dir[1];
continue;
} else if (this.cells[tr_here][td_here] === this.turn){
flag_mystone = true;
break;
} else{
flag_mystone = false;
break;
};
}
// なかったら次の方向へ
if (!flag_mystone){
return;
};
// 相手の石がある確認
td_here = td_num + dir[0];
tr_here = tr_num + dir[1];
while (this.check_end(tr_here, td_here)){
if (this.cells[tr_here][td_here] === 0-this.turn){
this.flag_cells[tr_here][td_here] = 1;
td_here += dir[0];
tr_here += dir[1];
} else{
break;
};
};
});
},
これでthis.flag_cells
に返せる石の位置を記述できました。
なお、ここでthis.check_end()
は盤面の端に来たかどうかを確認する関数です。
// 盤面の端に来たかどうかの確認
check_end: function(tr_num, td_num){
return 5>=tr_num & tr_num>=0 & 5>=td_num & td_num>=0;
},
次はそもそも駒を置けるかどうかの確認も含めて判定します。
// 駒を置けるか確認
check_put: function(tr_num, td_num){
if (this.cells[tr_num][td_num] != 0){
return false;
} else {
this.flag_cells = [
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
];
this.turn_over(tr_num, td_num);
return this.flag_cells.some(value => value.some(v => v===1));
};
},
まず、クリックされた位置に石がないことを確認します。なかった場合は、まず返せる石の位置this.flag_cells
を一度空にしてから、this.turn_over()
で返す石があるかどうかを調べます。
そして、this.flag_cells
のうち1(返せるもの)が一つでもあるかどうかをT/Fで返すような関数です。
さて、以上で、チェックする部分の記述が終わったので、実際に石を置く処理を書いてみましょう。
// 石を置く処理
put: function(tr_num, td_num){
if (this.check_put(tr_num, td_num)){
// ひっくり返す
for (let i=0; i<6; i++) {
for (let j=0; j<6; j++) {
if (this.flag_cells[i][j] === 1){
this.cells[i][j] = this.turn;
};
};
};
// 石を置く
this.$set(this.cells[tr_num], td_num, this.turn);
this.change_turn();
};
},
まず、this.check_put()
で石を置けるかどうか判定して、各マスに対して返せる場所は返すという構造です。
置き終わったら次はターンを入れ替える処理です。
// ターンを変える処理
change_turn: function(){
this.turn = 0 - this.turn;
// 終了判定
if (!this.cells.some(value => value.some(v => v===0))){
this.finish()
}
this.verification_put();
},
ターンは石の値に合わせて{白:-1, 黒:1}のようにしたいので、0から引くことでこれを行っています。ここで、もしすべてのマスが埋まっていたら、this.finish()
という関数で終了の処理を行うようにしており、またパスになるかどうかの確認等の処理をthis.verification_put()
で行うようにしておきます。
せっかくなので、パスになるかどうかの確認とともに、cpu対戦の機能を同時に追加してみます。this.flag_vscpu
をcpu対戦するかどうかのフラグとして以下のようにhtml上のラジオボタンで選択できるように記述します。
<form action="">
<input type="radio" name="cpu" id="cpu" value="cpu" v-model="flag_vscpu" v-on:click="vs_cpu()">
<label for="cpu" v-on:click="vs_cpu()">CPUと対戦</label><br>
<input type="radio" name="person" id="person" value="person" v-model="flag_vscpu">
<label for="person">自分で操作</label>
</form>
では、これを用いて置き場所の確認をしましょう。
// 置き場所の確認・cpuの指し手
verification_put: function(){
// 置き場所があるかを確認
let able_put = []
for (let i=0; i<6; i++) {
for (let j=0; j<6; j++) {
if (this.check_put(i, j)){
able_put.push([i, j]);
};
};
};
// 置く場所なかったら終了or手番を変える
if (able_put.length === 0){
document.getElementById("message").innerHTML = "置ける場所がありません";
if (this.flag_pass){
this.finish()
} else {
this.flag_pass = true
this.change_turn()
};
} else {
this.flag_pass = false
// cpuの番
if (this.flag_vscpu === "cpu" & this.turn === this.random){
let random_choice = able_put[Math.floor(Math.random() * able_put.length)];
setTimeout(this.put, 1000, random_choice[0], random_choice[1]);
};
};
},
able_put
における場所の配列が入れられるので、それの長さが0だったらパスという仕組みです。また、this.flag_pass
で2回以上パスが続いたら両者とも置けないので、試合を終了することにしています。
また、cpuの操作としては、cpuの番の時に、置ける場所からランダムに一つ選んでおくようにしています。setTimeout(関数, 時間[ms], 引数1, 引数2,...)
は、ほかの言語でいうところのsleepの機能を実装することで、cpuが置くまでに時間がかかるようにして視覚的にわかりやすくしています。
最後の終了した際の処理です。
// 終了時の処理
finish: function(){
let sum = this.cells.reduce((sum, element) => sum + element.reduce((sum2, element2) => sum2 + element2, 0), 0)
if (sum > 0){
document.getElementById("message").innerHTML = "黒の勝ちです";
} else if (sum < 0){
document.getElementById("message").innerHTML = "白の勝ちです";
} else {
document.getElementById("message").innerHTML = "引き分けです";
}
setTimeout(this.new_game, 1000);
},
石の和を数えることで+なら黒の勝ち、-なら白の勝ちと判定することができます。ここでも、setTimeout()
で新しいゲームを始めるまでに時間を止めています。
// 新しいゲームを開始
new_game: function(){
this.init_field();
this.cells.splice(this.cells.length-1, [0, 0, 0, 0, 0, 0]);
},
新しいゲームを始めるには盤面などを初期の設定に戻す必要があるので、それをthis.init_field()
関数に書き、表示を更新するために、Array.splice
メソッドを記述しておきます。
// 盤面の初期化処理
init_field: function(){
// 盤面
this.cells = [
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0,-1, 1, 0, 0],
[0, 0, 1,-1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
];
// 返せる駒の位置
this.flag_cells = [
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
];
// その他
this.turn = 1;
this.flag_pass = false;
this.random = Math.floor(Math.random() * 2) * 2 -1;
document.getElementById("message").innerHTML = "次は黒番です";
this.verification_put();
},
cpu対戦の際に黒白を決めるのは乱数で行っていますが、-1or1にしたいので、0or1の乱数を発生させて(Math.floor(Math.random() * 2)
)、それを2倍して-1しています。また、初手がcpuになる可能性を考えて駒を置ける場所の探索もしておきます。
あとは最初にも実行されるようにcreatedフックに登録しておきます。
created() {
this.init_field();
}
基本的にはこれで完成です。メッセージを書いたり形式を整えたりした完成版は https://github.com/eycjur/pages/blob/main/public/othello.html に上げておきます。
プロダクトURL:https://eycjur.github.io/pages/othello.html
2021年もよろしくお願いします。よいお年を!