マスターマインドとは
マスターマインド(Wikipedia 英語 / 日本語)というゲームがだいぶ前に市販されてまして、うちには Invicta 社の「Super Master Mind」の実物がまだあります。これは元祖の難度を上げたもので、穴(桁数)が 4 から 5、色数(候補の種類)が 6 から 8 に増えているものです。
このゲームの欠点は、出題する側が出された答えについて当たり判定をしなければならないところです。正確に判定を出すのがけっこうむずかしい。判定が間違っていれば、その後のゲームが無意味になってしまいます。すなわち、判定は機械にまかせたほうがよいゲームなのです。
そういうわけで、1995 年に自分で遊ぶために DOS のテキストエディタのマクロでマスターマインド、のようなもの、を作成しました。で、最近ときどき、あの昔のマスターマインドで遊んでみたいと思うようになり、このごろ PHP のスクリプトをちょこっと書くようになったので、移植してみることにしました。
PHP に移植しはじめてすぐ気がついたのは、これは本来、PHP より JavaScript で書くべきものだろう、ということです。また JavaScript を勉強したらそっちで作ろうと思います。でもとりあえず PHP で。
「Hit & Blow」あるいは「Bulls and Cows」というフレーズで検索すると、かなりの数の類似のスクリプトが作成されているのがわかります。しかし、ほとんどのスクリプトが「同じ数字は 2 度使わない」というルールになっています。以前に遊んでいた市販品のマスターマインドでは、同じ色を何度使ってもよい、というルールだったので、「同じ数字は使わない」のが不満でした。そこから、自分で作ってみようということになったわけです。
通常は 0 から 9 の間の数字のみで出題するのですが、16 進数での出題もしてみたいと思いつき、PHP に移植するにあたって出題文字種を自由に設定できるようにしてみました。マルチバイト対応で文字列を処理しているので、「ひらがな」はじめ、日本語の文字からも出題できます。(ただ、文字の種類がやたら多くても当たらないだけで面白くありません。)
今回作成したスクリプトはここで実際に動かしています。次の種類(設定のみ変更)がありますので、もしまだこのゲームが未体験でしたら、ぜひお試しください。
- よくある 4 桁数字バージョン
- 4 桁の 16 進数を当てるバージョン
(サーバーの PHP がマルチバイト文字処理対応でなくなったようなので、ここで動かすのは非マルチバイト対応のものに切り替えました。)
ゲーム画面はこんなふうになります。
このスクリプトの仕様は次のとおり。
- 同じ数字(文字)を複数回使える。全部同じ数字(文字)という場合もありうる。
- スクリプト内の数値を 1 か所変えるだけで、問題の桁数が変更できる。
- スクリプト内の文字列を 1 か所変えるだけで、出題に使用される文字種が変更できる。
- 当たり判定表示は市販ゲームのように、違うマークを個数だけ並べる方式。
- 一人用なので、複数回遊んだときは直前の成績を表示する。
- 正解に至らず「この問題をやめる」ボタンを押してギブアップした場合は、「きょうの戦績」の回数のところに「x」マークがつく。
- スクリプトファイル 1 つだけで動作する。記録は取らない。
- 各要素の配置はスタイルシートで。
スクリプト内容
スクリプト内容は次のとおり。PHP の highlight_file 関数を通して色分け表示をしています。何か不備がありましたら、ぜひお知らせください。また、もし気に入ってくださった場合、設置などはご自由にどうぞ。
<?php
// マスターマインド EX by Kaburaya 2008/09/18
/*
- 起動直後 = デフォルト = 状態は newgame でメッセージなし。
- done = 正解/ギブアップ直後 = 次の問題ボタン + 前回の履歴 + あれば戦績
> 新しい問題へボタンを押せば newgame に移行
> 履歴消去でセッション初期化
- continued = 未正解 = 次の問題ボタン + 履歴 + あれば戦績
> この問題をやめるボタンを押せば aborted > done
> 正解なら > done
> 不正解なら判定結果を記録して continued へループ
- newgame = 新しい問題生成 + 新しい問題メッセージ + あれば戦績
> continued に移行
左ブロックは経過表示用
右に入力窓(入力履歴でゲーム経過を隠さないため)
*/
// ---------- 設定 ----------
$script_file = 'mmind_ex.php'; // ファイル名に合わせる
$digits = 4; // 問題の桁数
$session_name = 'mmind_ex_' . $digits;
$title = '[ マスターマインド EX ]';
$mark1 = '★'; // HIT 表示に使用するマーク
$mark2 = '☆'; // BLOW 表示に使用するマーク
$source = '0123456789'; // 10 進数の並びを当てるときは、これを選択
// $source = '0123456789abcdef'; // 16 進数の並びを当てる。
// $source = '0123456789abcdefghijklmnopqrstuvwxyz'; // これも使用可能
// $source = 'あいうえおかきくけこさしすせそ'; // マルチバイト対応
$message1 = "<< $digits 桁の文字列を当ててください >>";
$button1 = 'abort'; // 中断用ボタン
$label1 = 'この問題をやめる';
$button2 = 'new'; // 次に進むボタン
$label2 = '新しい問題へ';
$button3 = 'reset'; // 履歴消去
$label3 = '履歴消去';
// ---------- セッション初期化 ----------
if (session_name() != $session_name) {
session_cache_limiter('nocache');
session_name("$session_name"); // ☆ セッション名設定
session_start(); // セッション開始
}
// ---------- 問題生成処理 ----------
function new_game($digits,$source) {
$max = mb_strlen($source);
for ($i=0; $i<$digits; $i++) { // ランダムな数を生成
$secret = $secret . mb_substr($source,mt_rand(0,$max-1),1);
}
$_SESSION['secret'] = $secret; // セッション変数に格納
$_SESSION['count'] = 0; // 回数カウント開始
$_SESSION['history'] = ''; // 履歴消去
$_SESSION['game_status'] = 'newgame'; // ゲーム進行状況を newgame に
}
// ---------- 入力窓提示処理 ----------
function main_form($script_file) {
print <<<_FORM_
<form name="answer" method="post" action="$script_file">
あなたの答え: <input type="text" name="guess" size="20" />
<input type="submit" value="送信"/>
</form>
_FORM_;
}
// ---------- 下部ボタン表示処理 ----------
function bottom_button($button,$label) {
print <<<_BUTTON_
<form method="post" action="$script_file">
<input type="hidden" name="guess" value="$button" />
<input type="submit" value="$label" />
</form>
_BUTTON_;
}
// ---------- 経過表示処理 ----------
function print_history() {
if ( $_SESSION['count'] > 0 ) {
print <<<_TABLE_
<table border="0" cellspacing="0" cellpadding="0">
<tr><td>回数 / </td><td>推測 / </td><td>判定</td></tr>
_TABLE_;
print $_SESSION['history'];
print "</table>\n";
}
}
// ---------- 戦績表示処理 ----------
function print_today() {
if ($_SESSION['today']) { // きょうの戦績があれば表示
print <<<_TABLE2_
<hr />
<table border="0" cellspacing="0" cellpadding="0">
<tr><td colspan="2" align="center">= きょうの戦績 =</td></tr>
<tr><td>回数 / </td><td>答え</td></tr>
_TABLE2_;
print $_SESSION['today'];
print "</table>\n";
}
}
// ---------- 答えあわせ処理 ----------
function num_eval($digits, $target, $guess, $mark1, $mark2) {
for ($i=0; $i<$digits; $i++) { // 同じ桁位置(HIT判定)を優先実行
if ($guess[$i] == $target[$i]) { // 数字が一致した場合(HIT)
$target[$i] = 'x'; // 当たり済みマークをつける
$guess[$i] = 'o'; // 使用済みマークをつける
$hit++;
}
}
for ($i=0; $i<$digits; $i++) {
for ($j=0; $j<$digits; $j++) {
if ($guess[$i] == $target[$j]) { // 数字が一致した場合(BLOW)
$target[$j] = 'y'; // 当たり済みマークをつける
$guess[$i] = 'o'; // 使用済みマークをつける
$blow++;
}
}
}
// $result に HIT の数、BLOW の数、当たりマーク文字列を格納
$result[] = $hit;
$result[] = $blow;
while ($hit) {
$hit--;
$string = $string . $mark1;
}
while ($blow) {
$blow--;
$string = $string . $mark2;
}
$result[] = $string;
return $result;
}
// ---------- ボタンクリックによる分岐 ----------
if ($_POST['guess'] == 'reset') {
$message2 = "履歴を消去しました。</p>\n<p>新しい問題です。</p>";
$_SESSION['today'] = '';
session_start();
$_SESSION['game_status'] = '';
} elseif ($_POST['guess'] == 'abort') {
$_SESSION['game_status'] = 'done';
$secret = $_SESSION['secret'];
$message2 = "問題を中断しました。答えは $secret です。";
$today = '<tr><td>' . $_SESSION['count'] . ' x</td><td>' . $_SESSION['secret']. "</td></tr>\n";
// きょうの成績履歴に追加
$_SESSION['today'] = $today . $_SESSION['today'];
} elseif ($_POST['guess'] == 'new') {
$_SESSION['game_status'] = 'newgame';
}
// ---------- 入力内容分析 ----------
switch($_SESSION['game_status']) {
case '':
new_game($digits,$source);
$_SESSION['game_status'] = 'continued';
break;
case 'newgame':
new_game($digits,$source);
$message2 = '新しい問題です。';
break;
case 'continued':
if ($_POST['guess'] == '') {
$message2 = '答えの入力がありませんでした。';
} else {
$guess_text = mb_substr($_POST['guess'], 0, $digits); // 指定文字数のみを先頭から取得
for ($i=0; $i<$digits; $i++) { // 問題と答えの文字列を配列に分割
$target[] = mb_substr($_SESSION['secret'], $i, 1);
$guess[] = mb_substr($guess_text, $i, 1);
}
$count = ++$_SESSION['count'];
$data = num_eval($digits, $target, $guess, $mark1, $mark2); // 答え合わせ処理
$text = "<tr><td>$count</td><td>$guess_text</td><td>$data[2]</td></tr>\n";
$_SESSION['history'] = $text . $_SESSION['history'];
if ($data[0] == $digits) {
$message2 = '正解です、おめでとう!';
$_SESSION['game_status'] = 'done';
$today = '<tr><td>' . $_SESSION['count'] . '</td><td>' . $_SESSION['secret']. "</td></tr>\n";
// きょうの成績履歴に追加
$_SESSION['today'] = $today . $_SESSION['today'];
} else {
$message2 = '';
}
}
}
// ---------- HTML 表示 ----------
print <<<_HEADER_
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>$title</title>
<style type="text/css">
body {font-family: Trebuchet MS,Arial,Verdana,Helvetica,sans-serif; margin: 30px;}
div#history {float: left; width: 20em; padding: 10px 20px;}
div#input {float: auto; padding: 10px 20px;}
div#reminder {float: auto; padding: 10px; clear: both; padding: 10px 20px; color: gray;}
table {margin-left: 20px;}
td {padding-left: 10px;}
hr {border: 0; height: 1px; color: #b0b0b0; background-color: #b0b0b0;}
</style>
</head>
<body onload="document.answer.guess.focus()">
<div id="container">
<div id="history">
_HEADER_;
print "<p>$message1</p>\n";
if ( $message2 ) { print "<p>$message2</p>\n"; }
print_history();
print_today();
print "</div>\n<div id=\"input\">\n<p>\n";
// ---------- 条件分岐による output ----------
switch($_SESSION['game_status']) {
case 'reset':
break;
case 'done':
bottom_button($button2,$label2);
bottom_button($button3,$label3);
break;
case 'newgame':
main_form($script_file);
$_SESSION['game_status'] = 'continued';
break;
case 'continued':
main_form($script_file);
if ( $_SESSION['count'] > 0 ) { bottom_button($button1,$label1); }
}
print <<<_REMINDER_
</p>
</div>
<div id="reminder">
<hr />
<ul>
<li>使用文字種は “$source” です。</li>
<li>同じ文字が重複して使われることがあります。</li>
<li>結果判定の $mark1 は数字と桁の一致 $mark2 は数字の存在のみ一致。</li>
<li>回数制限はありませんが、問題が解けないときは “$label1” ボタンを押すと、現在の問題の答えが表示されます。</li>
<li>このスクリプトの動作にはクッキーの許可が必要です。</li>
</ul>
</div>
_REMINDER_;
?>
</div>
</body>
</html>
2008/12/11 ― 稼働サンプルを非マルチバイト版に切替。
(サーバーの状態への対応のため)
(2008/09/19 - 2008/12/11)