JavaScriptの勉強のため、数字当てゲームを作りました。
遊んでみるのもよし、自作して私のものと比較してみるのもよしだと思います。
#本記事の環境
※PCに環境構築を行う必要はありません。
WEBブラウザ(Google Chome)
テキストエディタ
数字当てゲームとは
1対1の対戦ゲーム。0~9から3つを選択し並び替える(012~987)。プレイヤーは相手が選択した3つの数字の組み合わせと並びを推理する。交互に数字を言い合い、相手より早く特定したほうが勝ち。
数字と並びが合っている場合はEAT(イート)、数字だけが合っている場合はBITE(バイト)と相手に伝える。要するに、相手に「3(スリー)EAT」と言わせられれば勝ち。
※「EAT」、「BITE」の呼び方はモノによって様々ある。
本ゲームを題材としたテレビ番組やアプリゲームがある。←自作してみようと思ったきっかけ。
#要件・仕様
- 言語の勉強が主目的なので、見た目はこだわらない。
- 言語の勉強が主目的なので、処理は小分けにするし、コメントも残す。
- フレームワークは使わない。
- 勝敗くらいは表示する。
- 対戦相手はCPUにする。
- プレイヤーがCPUに申告する3つの数字はプルダウンから選択する(以下、画像の415)。選択後「判定」を選択して判定する。
- 相手に当てさせる3つの数字とその並びはランダムとする。決定したら、プレイヤー側の数値のみ表示する(以下、画像の817)。
- お互いが申告した数字と判定結果の履歴は「プレイヤーXXのメモ」に表示する。
成果物(画面)
所感
- ボタン、プルダウン、非同期での画面更新等、いくつか要素があるわりに、デザイン要素が少ないため言語学習に丁度よかった。
- 対戦相手のCPUの思考回路を強化する余裕がなかったのが心残り。上記の画像の通り、今のCPUはお粗末な点があります。改良の余地あり。
- 思いのほか、ソースコード量が多くなった。
成果物(ソースコード)
- 参考程度にしてください。軽くは動作確認済みです。
Number.html
<!DOCTYPE html>
<html>
<head>
<title>数字当て</title>
<style>
body {
background-color : #eee;
}
#start, #judge, #next {
font-size : 20px;
background-color : #fff;
text-align : center;
}
#result, #turn {
font-size : 20px;
background-color : #fff;
}
#numbers {
font-size : 20px;
}
#number0, #number1, #number2 {
font-size : 20px;
}
#player00Info, #player01Info {
font-size : 20px;
}
#memos {
font-size : 20px;
}
</style>
</head>
<body>
<div>
<div>
<span id="button"></span>
<p id="result"></p>
<p id="turn"></p>
<p id="numbers"></p>
<p id="player00Info"></p>
<p id="player01Info"></p>
<p id="memos"></p>
</div>
</div>
<script language="JavaScript">
// 数字当て情報
const MAX_PLAYER = 2;
const MAX_HAND = 3;
const MAX_NUM = 9;
const MIN_NUM = 0;
const PLAYER_USER = 0;
const PLAYER_CPU = 1;
// その他
//const FLAG_DEBUG = true;
const FLAG_DEBUG = false;
// カード
function Card(num) {
// プロパティ
this.num = num;
}
// デッキ
function Deck() {
// プロパティ
this.deck = [];
// 初期化処理
this.init = function() {
this.deck = [];
return;
};
// デッキ作成処理
this.make = function() {
let card;
for (let num = MIN_NUM; num <= MAX_NUM; num++) {
card = new Card(num);
this.deck.push(card);
}
return;
};
// デッキシャッフル処理
this.shuffle = function() {
let random;
let work;
for (let loop = 0; loop < this.deck.length; loop++) {
random = Math.floor(Math.random() * this.deck.length);
work = this.deck[0];
this.deck[0] = this.deck[random];
this.deck[random] = work;
}
return;
};
// ドロー処理
this.draw = function() {
return this.deck.shift();
};
}
// 手札
function Hand() {
// プロパティ
this.hand = [];
// 初期化処理
this.init = function() {
this.hand = [];
return;
};
// 手札セット処理
this.set = function(card) {
this.hand.push(card);
return;
};
// 手札全取得処理
this.getAll = function() {
return this;
};
// 手札シャッフル処理
this.shuffle = function() {
let handInfo = this.getAll();
let random;
let work;
for (let loop = 0; loop < handInfo.hand.length; loop++) {
random = Math.floor(Math.random() * handInfo.hand.length);
work = handInfo.hand[0];
handInfo.hand[0] = handInfo.hand[random];
handInfo.hand[random] = work;
}
return;
};
}
// プレイヤー
function Player() {
// プロパティ
this.results = [];
this.results["win"] = 0;
this.results["lose"] = 0;
this.cpu = true;
// 初期化処理
this.init = function() {
return;
};
// CPUフラグ設定処理
this.setCpu = function(flag) {
// CPUフラグを設定する。trueがCPU。
this.cpu = flag;
return;
};
}
// メモ
function Memo() {
// プロパティ
this.memos = [];
this.memos["num"] = [];
this.memos["cnts"] = [];
// 初期化処理
this.init = function() {
this.memos = [];
this.memos["num"] = [];
this.memos["cnts"] = [];
};
// 数字セット処理
this.setNumber = function(number) {
this.memos["num"].push(number);
return;
};
// 判定結果セット処理
this.setResults = function(eat, bite) {
this.memos["cnts"]["eat"] = eat;
this.memos["cnts"]["bite"] = bite;
return;
};
// メモ全取得処理
this.getAll = function() {
return this;
};
}
// 桁数整形処理
function spacePadding(num){
return (Array(2).join(" ") + num).slice(-2);
}
// 桁数整形処理
function zeroPadding(num){
return (Array(2).join("0") + num).slice(-2);
}
// 数値比較処理
function compare(x, y){
return x - y;
}
// 数字当て
function Number () {
// プロパティ
let cntTurn = 0;
let deck = new Deck();
let hands = [];
let players = [];
let memos = [];
for (let loop = 0; loop < MAX_PLAYER; loop++) {
hands[loop] = new Hand();
players[loop] = new Player();
memos[loop] = [];
}
// 初期化処理
this.init = function() {
// ターン数を初期化する。
cntTurn = 0;
// 各プレーヤーごとにプレイヤー、手札、メモの初期化処理を呼び出す。
for (let loop = 0; loop < MAX_PLAYER; loop++) {
players[loop].init();
hands[loop].init();
memos[loop] = [];
}
// CPUフラグ設定処理を呼び出す。
players[PLAYER_USER].setCpu(false);
return;
};
// カードイメージ作成処理
this.makeCardImg = function(card, playerId, index) {
// カードイメージを作成する。
let cardImg = "";
cardImg += "<span id='card' ";
cardImg += "name='" + String(playerId) + "_" + String(index) + "' ";
cardImg += "style='color:black; ";
cardImg += "background-color:white;'>";
cardImg += card.num;
cardImg += "</span>";
return cardImg;
};
// 手札表示共通処理
this.outputHandPCommon = function(playerId, handInfo) {
let detail = "";
handInfo.hand.forEach(function(card, index) {
if (FLAG_DEBUG == true) {
// カードイメージ作成処理を呼び出す。
//alert("outputHandPCommon card:" + card.num + " playerId:" + playerId + " index:" + index);
detail += game.makeCardImg(card, playerId, index);
} else {
if (players[playerId].cpu == false) {
// カードイメージ作成処理を呼び出す。
detail += game.makeCardImg(card, playerId, index);
} else {
detail += "<span id='card' ";
detail += "name='" + String(playerId) + "_" + String(index) + "' ";
detail += "style='color:black; ";
detail += "background-color:black;'>";
detail += "♠";
detail += "</span>";
}
}
detail += " ";
});
return detail;
};
// 手札表示処理
this.outputHandP = function() {
// 各プレーヤーごとに手札表示共通処理を呼び出す。
let handInfo = hands[0].getAll();
let work = this.outputHandPCommon(0, handInfo);
// 処理結果を設定する。
let id = "player" + String(zeroPadding(0)) + "Info";
document.getElementById(id).innerHTML = work;
return;
};
// 手札自動決定処理
this.selectHandCpu = function() {
// デッキの初期化処理を呼び出す。
deck.init();
// デッキ作成処理を呼び出す。
deck.make();
// 作成したデッキをシャッフルする。
deck.shuffle();
for (let loop = 0; loop < MAX_HAND; loop++) {
// デッキからカードをドローする。
let card = deck.draw();
// 手札セット処理を呼び出す。
hands[PLAYER_CPU].set(card);
}
return;
};
// 手札手動決定処理
this.selectHandUser = function() {
// デッキの初期化処理を呼び出す。
deck.init();
// デッキ作成処理を呼び出す。
deck.make();
// 作成したデッキをシャッフルする。
deck.shuffle();
for (let loop = 0; loop < MAX_HAND; loop++) {
// デッキからカードをドローする。
let card = deck.draw();
// 手札セット処理を呼び出す。
hands[PLAYER_USER].set(card);
}
return;
};
// ゲーム終了判定実処理
this.isFinCore = function(playerId) {
// 3EATが存在するか調べる。
let detail;
let work = memos[playerId].find(function(memo) {
detail = memo.getAll();
return detail.memos["cnts"]["eat"] == MAX_HAND;
});
// 存在した場合はTRUEを設定する。
let result = false;
if (work != undefined) {
result = true;
}
return result;
};
// ゲーム終了判定処理
this.isFin = function() {
let win = -1;
for (let loop = 0; loop < MAX_PLAYER; loop++) {
// ゲーム終了判定実処理を呼び出す。
result = this.isFinCore(loop);
if (result == true) {
// 勝利プレイヤーのIDを設定する。
win = loop;
break;
}
}
return win;
};
// ゲーム終了処理
this.fin = function(win) {
// 開始ボタンを活性に変更する。
document.getElementById("start").disabled = false;
// 判定ボタンを非活性に変更する。
document.getElementById("judge").disabled = true;
// 次へボタンを非活性に変更する。
document.getElementById("next").disabled = true;
// 表示文言を作成する。
let work = "<span ";
work += "style='color:red; ";
work += "background-color:white;'>";
if (win == PLAYER_USER) {
work += "プレイヤー" + zeroPadding(win % MAX_PLAYER + 1) + "の勝ち";
players[PLAYER_USER].results["win"]++;
players[PLAYER_CPU].results["lose"]++;
} else {
work += "プレイヤー" + zeroPadding(win % MAX_PLAYER + 1) + "の勝ち";
players[PLAYER_CPU].results["win"]++;
players[PLAYER_USER].results["lose"]++;
}
work += "</span>";
// 作成した文言を表示する。
document.getElementById("turn").innerHTML = work;
// 結果表示処理を呼び出す。
this.outputResult();
return;
};
// ゲーム開始処理
this.start = function() {
// 初期化処理を呼び出す。
this.init();
// 開始ボタンを非活性に変更する。
document.getElementById("start").disabled = true;
// 判定ボタンを活性に変更する。
document.getElementById("judge").disabled = false;
// 手札自動決定処理を呼び出す。
this.selectHandCpu();
// 手札手動決定処理を呼び出す。
this.selectHandUser();
// 手札表示処理を呼び出す。
this.outputHandP();
// プルダウン表示処理を呼び出す。
this.outputPullDown();
// メモ表示処理を呼び出す。
this.outputMemo();
// 手番表示処理を呼び出す。
this.outputTurn();
return;
};
// ターン変更処理
this.changeTurn = function() {
// ゲーム終了判定処理を呼び出す。
result = game.isFin();
if (result != -1) {
// ゲーム終了処理を呼び出す。
game.fin(result);
} else {
// ターンを切り替える。
cntTurn++;
// 手札表示処理を呼び出す。
this.outputHandP();
// 手番表示処理を呼び出す。
this.outputTurn();
// 手番によって処理を分岐する。
if (players[cntTurn % MAX_PLAYER].cpu == true) {
// 手番がCPUである場合
// 判定ボタンを非活性に変更する。
document.getElementById("judge").disabled = true;
// 次へボタンを活性に変更する。
document.getElementById("next").disabled = false;
// ユーザの次へボタン操作イベント待ち。
// ターン変更処理を呼び出す。
game.next();
} else {
// 手番がユーザである場合
// 判定ボタンを活性に変更する。
document.getElementById("judge").disabled = false;
// 次へボタンを非活性に変更する。
document.getElementById("next").disabled = true;
// ユーザのカード選択イベント待ち。
}
}
return;
};
// 探索処理
this.search = function(playerId, number) {
// 指定した値が存在すれば位置情報を設定する。
let handInfo = hands[playerId].getAll();
let result = handInfo.hand.findIndex(function(card) {
return card.num == number;
});
//alert("search result=" + result);
return result;
};
// メモ表示処理
this.outputMemo = function() {
// 選んだ数字や判定結果を表示する。
let detail;
let length = memos[PLAYER_USER].length;
let work = "<span>";
work += "<span>";
work += "プレイヤー" + zeroPadding(PLAYER_USER + 1) + "のメモ ";
work += "</span>";
work += "<span>";
work += "プレイヤー" + zeroPadding(PLAYER_CPU + 1) + "のメモ";
work += "</span>";
work += "<br>";
for (let index = 0; index < length; index++) {
for (let playerId = 0; playerId < MAX_PLAYER; playerId++) {
if (memos[playerId][index] != undefined) {
detail = memos[playerId][index].getAll();
work += "<span>";
work += "[" + detail.memos["num"] + "] ";
work += detail.memos["cnts"]["eat"] + " EAT ";
work += detail.memos["cnts"]["bite"] + " BITE ";
work += "</span>";
work += " ";
}
}
work += "<br>";
}
work += "</span>";
let id = "memos";
document.getElementById("memos").innerHTML = work;
return;
};
// 判定共通処理
this.judgeCommon = function(my, your, numbers) {
let result;
let cnts = [];
cnts["eat"] = 0;
cnts["bite"] = 0;
let length = memos[my].length;
memos[my][length] = new Memo();
numbers.forEach(function(number, index) {
// 数字セット処理を呼び出す。
memos[my][length].setNumber(number);
// 探索処理を呼び出す。
result = game.search(your, number);
if (result != -1) {
if (result == index) {
// 位置が合っているためEAT。
cnts["eat"]++;
} else {
// 位置が合っていないためBITE。
cnts["bite"]++;
}
} else {
// ハズレ
}
});
// 判定結果セット処理を呼び出す。
memos[my][length].setResults(cnts["eat"], cnts["bite"]);
// メモ表示処理を呼び出す。
this.outputMemo();
// ターン変更処理を呼び出す。
game.changeTurn();
return;
};
// 並び替え処理
this.randomSort = function(str) {
let random;
let work;
for (let loop = 0; loop < str.length; loop++) {
random = Math.floor(Math.random() * str.length);
work = str[0];
str[0] = str[random];
str[random] = work;
}
return str;
};
// 数字自動選択処理
this.selectNumberCpu = function() {
let card;
let numbers = [];
let length;
let detail;
let notHits = [];
let hits = [];
let work;
let work2;
length = memos[PLAYER_CPU].length;
// 0EAT、OBITEとなった時の数値を洗い出す。
work = memos[PLAYER_CPU].filter(function(memo) {
detail = memo.getAll();
return (detail.memos["cnts"]["eat"] == 0) && (detail.memos["cnts"]["bite"] == 0);
});
if (work != undefined) {
work.forEach(function(data) {
data.getAll();
notHits.push(data.memos["num"][0]);
notHits.push(data.memos["num"][1]);
notHits.push(data.memos["num"][2]);
});
}
// EAT、BITEの合計でMAX_HANDとなった時の数値を洗い出す。
work = memos[PLAYER_CPU].find(function(memo) {
detail = memo.getAll();
return detail.memos["cnts"]["eat"] + detail.memos["cnts"]["bite"] == MAX_HAND;
});
if (work != undefined) {
work.getAll();
hits.push(work.memos["num"][0]);
hits.push(work.memos["num"][1]);
hits.push(work.memos["num"][2]);
}
while (1) {
if (hits.length != 0) {
// 3つまで絞り込んでいる場合
// 並び替え処理を呼び出す。
numbers = this.randomSort(hits);
} else if (cntTurn < 6) {
//alert("序盤 : " + cntTurn);
// 0~9の数字をランダムで取得する。(デッキのシャッフル処理を流用。)
deck.init();
deck.make();
deck.shuffle();
for (let loop = 0; loop < MAX_HAND; loop++) {
card = deck.draw();
work = memos[PLAYER_CPU].find(function(memo) {
detail = memo.getAll();
work2 = detail.memos["num"].find(function(number) {
// 既に宣言済みの数字である場合
return number == card.num;
});
if (work2 != undefined) {
// 既に宣言済みの数字である場合
return true;
}
});
if (work != undefined) {
// 既に宣言済みの数字である場合
loop--;
continue;
}
numbers[loop] = card.num;
}
} else if (notHits.length < MAX_HAND * 2) {
//alert("候補が多い場合 : " + notHits.length);
while (1) {
// 0~9の数字をランダムで取得する。(デッキのシャッフル処理を流用。)
deck.init();
deck.make();
deck.shuffle();
for (let loop = 0; loop < MAX_HAND; loop++) {
card = deck.draw();
work = notHits.filter(function(notHit) {
return notHit == card.num;
});
if (work != "") {
// 使用されていないと確定している数値の場合は候補から外す。
loop--;
continue;
}
numbers[loop] = card.num;
}
numbers.sort(this.compare);
//alert("numbers:" + numbers);
work = memos[PLAYER_CPU].find(function(memo) {
detail = memo.getAll();
work2 = detail.memos["num"].slice();
work2.sort(this.compare);
//alert("work2:" + work2);
let loop;
for (loop = 0; loop < MAX_HAND; loop++) {
if (numbers[loop] != work2[loop]) {
// 既に宣言済みの数字である場合(並びは不問)
break;
}
}
if (loop >= MAX_HAND) {
return false;
} else {
// 既に宣言済みの数字である場合(並びは不問)
return true;
}
});
if (work != undefined) {
//alert("宣言済の組み合わせでなかった場合");
//alert("numbers:" + numbers);
work = memos[PLAYER_CPU].find(function(memo) {
//alert("false");
detail = memo.getAll();
if (detail.memos["cnts"]["eat"] + detail.memos["cnts"]["bite"] >= 2) {
// EAT、BITEの合計が2以上である場合
work2 = numbers.find(function(number) {
for (let loop = 0; loop < MAX_HAND; loop++) {
if (number == detail.memos["num"][loop]) {
// 当たりの数字である可能性があるため除外する。
//alert("index:" + loop + " num:" + detail.memos["num"][loop]);
return true;
}
}
});
if (work2 == undefined) {
//alert("false");
return false;
} else {
// 当たりの数字である可能性があるため除外する。
//alert("true");
return true;
}
}
});
if (work == undefined) {
break;
}
}
}
} else {
//alert("候補が少ない場合 : " + notHits.length);
// 0~9の数字をランダムで取得する。(デッキのシャッフル処理を流用。)
deck.init();
deck.make();
deck.shuffle();
for (let loop = 0; loop < MAX_HAND; loop++) {
card = deck.draw();
work = notHits.filter(function(notHit) {
return notHit == card.num;
});
if (work != "") {
// 使用されていないと確定している数値の場合は候補から外す。
loop--;
continue;
}
numbers[loop] = card.num;
}
}
work = memos[PLAYER_CPU].find(function(memo) {
detail = memo.getAll();
work = numbers.filter(function(number, index) {
return number == detail.memos["num"][index];
});
return work.length == MAX_HAND;
});
if (work == undefined) {
//alert("宣言済の組み合わせでなかった場合");
break;
}
}
return numbers;
};
// 判定処理
this.judgeCpu = function(numbers) {
// 判定共通処理を呼び出す。
game.judgeCommon(PLAYER_CPU, PLAYER_USER, numbers);
return;
};
// 判定処理
this.judgeUser = function() {
// プルダウンで選んだ数字を取得する。
let pullDown;
let numbers = [];
for (let index = 0; index < MAX_HAND; index++) {
pullDown = document.getElementById("number" + index);
numbers[index] = pullDown.value;
}
// 判定共通処理を呼び出す。
game.judgeCommon(PLAYER_USER, PLAYER_CPU, numbers);
return;
};
// 判定結果セット処理
this.setResults = function(eat, bite) {
this.memos["cnts"]["eat"] = eat;
this.memos["cnts"]["bite"] = bite;
return;
};
// ターン進行処理
this.next = function() {
let numbers = [];
// 数字自動選択処理を呼び出す。
numbers = this.selectNumberCpu();
// 判定処理を呼び出す。
this.judgeCpu(numbers);
return;
};
// プルダウン表示処理
this.outputPullDown = function() {
let work = "NUMBER : ";
let select = "";
for (let index = 0; index < MAX_HAND; index++) {
work += "<select id='number" + String(index) + "'>";
for (let loop = 0; loop <= MAX_NUM; loop++) {
if (loop == index) {
select = " selected";
} else {
select = "";
}
work += "<option value='" + String(loop) + "'"
work += select + ">" + String(loop) + "</option>";
}
work += "</select>";
}
document.getElementById("numbers").innerHTML = work;
return;
};
// 結果表示処理
this.outputResult = function() {
let work = "<p id='result' style='color:red'>";
for (let loop = 0; loop < MAX_PLAYER; loop++) {
work += "プレイヤー" + String(zeroPadding(loop + 1)) + ":";
work += String(spacePadding(players[loop].results["win"])) + "勝 ";
work += String(spacePadding(players[loop].results["lose"])) + "負" + "<br>";
}
work += "</p>";
document.getElementById("result").innerHTML = work;
return;
};
// 手番表示処理
this.outputTurn = function() {
let work = "<span>";
work += "プレイヤー" + zeroPadding(cntTurn % MAX_PLAYER + 1) + "の手番";
work += "</span>";
document.getElementById("turn").innerHTML = work;
return;
};
// ボタン表示処理
this.outputButton = function() {
let work = "<span>";
work += "<input type='button' id='start' value='開始' onclick='game.start()'>";
work += "<input type='button' id='judge' value='判定' disabled=true onclick='game.judgeUser()'>";
work += "<input type='button' id='next' value='次へ' disabled=true onclick='game.next()'>";
work += "</span>";
document.getElementById("button").innerHTML = work;
return;
};
}
// 初期化処理を呼び出す。
let game = new Number();
game.init();
// ボタン表示処理を呼び出す。
game.outputButton();
// 結果表示処理を呼び出す。
game.outputResult();
</script>
</body>
</html>