謝辞
タイトルに「独自ルール」とありますが、既出だったらごめんなさい。
また、妥協した点がいくつもありますが、初心者が自力で頑張ったということでお許しください。
記事の内容
- 前書き&ゲームについて
- 作った理由
- 普通のオセロ開発の流れ
- 独自ルールへシフト
- 初GitPush
- この制作を通して分かったこと
前書き&ゲームについて
ようやく自分のゲームを公開することが出来ました!!!
ルールは
- 基本的な操作は普通のルールと同じ
- 勝利判定だけ異なり、
背景が自分の色のマスの数字の和
が得点となり、得点が大きかった方が勝ち
です。
実際のプレイ画面はこんな感じです
中心の点数が1点なのに対し角の点数が25点と、角最強なモードにしました。
下に両者の取っているマスの数、そのときの得点が表示されるようになっています。
また、右のサイドバーにあるものは、
- 最初から
- ページのリロード用
- 一手飛ばす
- どのマスにも置けないときに押す用
・・・本来は自動で判定させたかった
- どのマスにも置けないときに押す用
- 勝利判定へ
- 勝敗を出したいとき用
・・・本来は自動で判定させたかった
- 勝敗を出したいとき用
となっております。
まだ完成度の低い部分がいくつもありますが、99%自力ということもあるので、とりあえず満足してます!
ちなみに残りの1%はこちらで、
質問に頼りました。
作った理由
元々C言語でもゲームを作っていた私は、ゲームを作ることが一番勉強になると思っていて、JSでもその流れでいました。また、オセロが登竜門だと思っていました。(勝手に)
これらの考えから、そろそろオセロが作れるなの段階で作り始めました。
開発までの流れ
普通ののオセロを作る
まずは普通のオセロを作りました。
JSの解説
以下は普通のオセロの解説で、今回のルールではありません。
プログラミングの流れは大体下のソースコードにコメントアウトで書いてありますので、ご覧ください。
こちら(198行もあるよ)
/*
メモ
y座標は盤面の下にいくほど大きくなる
x座標はふつう
自分のstoneの色:stone[which]
相手のstoneの色:stone[(which-1)*(-1)+1]
コメントに関して
//ゲームの処理など大事な部分に関する記述
////割とどうでも良い記述や予備知識
*/
const toCell = (x,y) => {
//x座標, y座標を引数に、それらを座標に持つセルを返す関数。
//x, yは数値
////.toStringはなくても良いみたい。
return document.getElementById((10*x+y).toString(10));
}
const searchSpecificDirection = (x,y,direction,which) => {
////関数名・・・特定の方向を調べる
let wantToPut = toCell(x,y);
//クリックしたマスを取得。
sandwichedCells = [];//初期化
howManySearches = 1;//初期化
judge = 1;//初期化
//クリックしたマスを原点とした、directionのxy座標。
////見かけのx,y座標という意味でapparent
apparentX = direction-(3*(Math.floor(direction/3)))-1;
apparentY = (direction-(apparentX+1))/3-1;
for(howManySearches=1;judge==1;howManySearches++){
//調べるマス
subCellX = x+apparentX*(howManySearches+1);
subCellY = y+apparentY*(howManySearches+1);
subjectCell = toCell(subCellX,subCellY);
////subjectCellXではなくsubCellXとすることで、読みやすいだけでなく、VSCodeの機能である予測変換がしやすくなる。
//その直前のマス。
////接頭句preには「前の」の意がある
preCellX = x+apparentX*howManySearches;
preCellY = y+apparentY*howManySearches;
previousCell = toCell(preCellX,preCellY);
//配列に追加
sandwichedCells.push(previousCell);
if(subjectCell.innerText==stone[(which-1)*(-1)+1]){
//調べるマスの色が相手の色だったら、
judge = 1;
//その先のマスを調べる
}else if(subjectCell.innerText==stone[which]){
//調べるマスの色が自分の色だったら、例えば○●○のようになっているはずなので、
sandwichedCells.push(wantToPut);
//置きたいマスも配列に入れる
let tmp1 = sandwichedCells.length;
//その時点での配列の長さを保持しておく
////(直後のfor文で使うためだが、ii<=~~.lengthとしてしまうと、forの1回の処理が終わるたびにそれが変わってしまうため、別の変数に保持しておく必要がある。)
for(let ii=1;ii<=tmp1;ii++){
sandwichedCells.shift().innerText = stone[which];
//オセロの石をひっくり返す、置く処理
}
judge = 0;//ループから抜ける
return 1;
}else if(subjectCell.innerText===stone[1]){
//調べるマスの色(?)が""だったら、
judge = 0;//ループから抜ける
}
}
return 0;
}
const countEachNumber = () => {
countBlack = 0;
countWhite = 0;
for(let j=1;j<=8;j++){
for(let i=1;i<=8;i++){
let blackOrWhite = toCell(j,i).innerText;
if(blackOrWhite=="●"){
countBlack++;
}else if(blackOrWhite=="○"){
countWhite++;
}
}
}
if(countBlack==0){
finalResultDiv.innerText ="白の勝利!";
}else if(countWhite==0){
finalResultDiv.innerText = "黒の勝利!";
}
black.innerText = countBlack;
white.innerText = countWhite;
}
const searchSurroundingEight = (xx,yy,which) => {
ans.length=0;//初期化
//置きたいマスの周囲8マスについて、相手の色の石が存在するか調べる
/*
ansの配列の添え字
0 1 2
3 4 5
6 7 8
存在する=>1、存在しない=>0
*/
for(let b=-1;b<=1;b++){
for(let a=-1;a<=1;a++){
if(toCell(xx+a, yy+b).innerText == stone[(which-1)*(-1)+1]){//調べるマスに相手の色の石が存在したら、
ans.push(1);
//配列ansに1を追加。
}else{//そうでなければ、
ans.push(0);
//配列ansに0を追加。
}
}
}
if(ans.includes(1)==true){
//配列ansに1が存在したら => 周囲8マスに相手の石が存在したら、
let indices = [];//初期化
let result = [];//初期化
let idx = ans.indexOf(1);//最初から探索し、要素と引数が一致した最初の要素の添え字が返される。1つも無ければ-1が返される。
while (idx != -1) {//1が存在したら、
indices.push(idx);//配列indicesに追加。
idx = ans.indexOf(1, idx + 1);//第2引数は探索の開始位置。
}
let tmp2 = indices.length;//本コード48行目の、変数tmp1と同じ役割。
for(index=0;index<=tmp2-1;index++){
result.push(searchSpecificDirection(xx,yy,indices.shift(),which));
//ひっくり返ったかどうかの判定に使う。関数searchSpecificDirectionの戻り値を配列resultに追加する。
////変数 = 配列名.shift() で,配列名の最初の要素を取り出し、変数に格納する。
}
if(result.includes(1)){
//配列resultの要素に1が存在したら、=> ひっくり返ったら
cnt++;//ターンが進む
countEachNumber();
}else{
alert("そのマスには置けません。");
}
}else{
//配列ansに1が存在しなかったら => 周囲8マスに相手の石が存在しなかったら、
alert("そのマスには置けません。");
}
console.log(ans);
}
const clickCell = (e) => {//144行目、セルをクリックしたときの処理。
if(e.target.innerText==stone[0]||e.target.innerText==stone[2]){
//クリックしたセルにすでに石が置かれていたら
alert("そのマスは埋まっています。");
}else{
//そうでなければ、
let x = Math.floor(Number(e.target.id)/10);
let y = Number(e.target.id)-10*x;
//クリックしたセル(td)のidから、そのx座標を求めた。
////Number()は文字列を数値に変換するメソッド、
////Math.floor()は小数点以下切り捨てのためのメソッド(今回は)。
////C言語で演算子「/」は商を求めるが、JSの演算子「/」は余りが出ない
searchSurroundingEight(x,y,cnt%2*2);
//cnt%2 => 0 or 1
//cnt%2*2 => 0 or 2
//stone[]の添え字として使う引数
}
}
//ゲーム処理に使う変数などの宣言。どこに宣言するのが正攻法なのかわかりません。
let ans = new Array;
let cnt = 1;
let stone = ["○","","●"];
let index;
let countBlack, countWhite;
let black = document.getElementById("blackPoints");
let white = document.getElementById("whitePoints");
let apparentX, apparentY, subCellX, subCellY, previousCellX, previousCellY, subjectCell, previousCell;
let sandwichedCells = new Array;
let howManySearches; let judge;
//以下、web表示用
let field = document.getElementById("playField");
let table = document.createElement("table");
let finalResultDiv = document.getElementById("finalResult");
for(let i=0;i<=9;i++){
let tr = document.createElement("tr");
let fragment = new DocumentFragment();
/*
ひとつのオブジェクトに大量に子要素を挿入したい場合(今回ならtrに4つのtdを挿入)、fragmentというオブジェクトを作成し、そこに子要素を追加し、fragmentを挿入したい親要素に挿入したほうが実装が早いらしい。
けど私もここでtableに4つのtrを挿入する際に手を抜いてfragmentを使わずにやってます。使ってみたかっただけ
*/
for(let j=0;j<=9;j++){
//オセロ盤面は8*8マスであるが、探索の記述を減らすために10*10として、見えない枠を作った。////実際web上でクリックすると何かが起こる。
let td = document.createElement("td");
td.id=(10*j+i).toString(10);
////数値.toString(10)数値を10進数として認識し、それを文字列に変換する
////.toStringなくても普通に動いたからidて数値でもよさそう
td.innerText = stone[1];
//stone[1] => ""
//空文字に設定。後々のためnull、undefinedと区別したかった。
/* 参考(蛇足?)
console.log(""==null) => false
console.log(""===null) => false
console.log(""==undefined) => false
console.log(""===undefined) => false
console.log(null==undefined) => true
console.log(null===undefined) => false
*/
if(i==0||i==9||j==0||j==9){
td.className="border";
//class名を設定することで、CSSでデザインを操作しやすくしている。
}
td.addEventListener("click",clickCell,false);
//tdにイベント属性を追加している。
////第3引数のfalseはお決まり。基本これでよい。なんなら省略してもよい。
////onClickよりこっちのほうがいいらしい。
fragment.appendChild(td);
}
tr.appendChild(fragment);
table.appendChild(tr);
}
field.appendChild(table);
let restartBut = document.getElementById("restart");
restartBut.addEventListener("click",()=>{
window.location.reload();
},false)
let skipBut = document.getElementById("skip");
skipBut.addEventListener("click",()=>{
cnt++;
},false)
//オセロの盤面は中心4マスに予め石が置かれているので、その記述。
toCell(4,4).innerText=stone[2];
toCell(5,5).innerText=stone[2];
toCell(4,5).innerText=stone[0];
toCell(5,4).innerText=stone[0];
クラスメイトに見せようと思い、説明口調で余計なことをコメントしまくっています。
おおざっぱに言うと、
- クリックされたマスの周囲8マスを調べる
- 相手の色の石が存在した場合、その方向を記録しておく
- 2で記録した方向のさらに一つ先を調べ、相手の色ならさらにその先、自分の色なら4へ進む
- 3で調べた結果、ひっくり返す石があるならひっくり返す
という流れです。
自分なりの工夫(妥協)
- 片や角の周囲の8マスを調べる際に楽なので、本来のオセロの8×8に加え、上下左右に1マス分ずつ余白を与え、10×10マスで作りました。
- 勝利判定やスキップをあきらめ、ボタンにしました。
独自ルールへシフト
せっかくコードを自力で書くなら、オリジナルなルールにしてさらに自分だけのコードにしよう(?)ということで、得点制にしようと決めました。
普通のルールではinnerText
によって石を区別したのに対し、独自ルールではbackground-color
によって変えています。
ソースコードはこちらをご覧ください。
本来は
- 内側が強いモード
- 右半分が強いモード
- 勝利判定のタイミングで点数がわかるモード
- それらのモードを選べる画面
などを追加したかったですが、これ以上やっても時間に対する得られる知識量が少ないだろうということで辞めました。
初GitPush
これらの記事を参考にしました。
ひたすらエラーが出たらそれをコピペして検索の繰り返しでなんとかなりました。
まだコマンドの意味など分からないことだらけなので、とりあえずやることができればいいやの精神です。
この制作を通して分かったこと
- オセロくらいの規模のプログラミングでも、最初に計画を建てて始めることの必要性
- のちのリファクタリングや、機能追加が圧倒的にやりやすくなりそう。
- デバッグの大変さ
- ブラックボックステストと言ったら大袈裟かもしれないが、いろいろな置き方で石を置くことで、普通にプレイしているだけでは見つからないようなバグが見つかった。
以上!
ここまで読んでくださった方、実際にプレイしてみてくださった方ありがとうございました。
できた瞬間のうれしさのまま記事を書いたので、すごくまとまりのない文章となりすみません。
あくまで趣味の範囲でしかありませんが、これからもゲームを作って勉強しようと思います。