star back image
people4
電飾 電飾
moon
astronaut

【WEBアプリ】じゃんけん!オンラインゲーム

BLOG AIPHPWEBアプリWEBログイベント
読了約:32分

「じゃんけん」したことありますか。
2人以上で面と向かってポンポンやるアレです。

最初はグーのやつですね。

ですです。

検索しました。

参考:日本じゃんけん協会
https://japan-rps.jimdofree.com/

「最初はグー、じゃんけんぽん!」などの掛け声を発しつつ、「石(グー)」、「鋏(チョキ)」、「紙(パー)」のうちから、各プレーヤーが、同時に好きな手を出す。 それぞれが出した手の種類によって、プレーヤー間の「勝ち」、「負け」、「あいこ」が決まる。

すごく単純で簡単なゲームですね。

今回、どうしてじゃんけん?

【経緯】なぜじゃんけん

じゃんけんが必要だったのです。そんな時ありませんか。
たとえば。。

  • 誰が支払うかじゃんけんで決める
  • バンジーする順番をじゃんけんで決める
  • 好きなドーナッツを選ぶ順番を決める

などなど。

そういう時、じゃんけんは便利です。

でも、テレワークとか、ネット経由のイベントなど、面と向かってじゃんけんできない。

そんな時、ブラウザでできるじゃんけんゲームを作ろうと思いました。

【調査】参考ブログ探し

じゃんけんプログラムはどうやって作ればいいのでしょう。
検索していろんなブログを拝見しました。

一番近い内容が「複数人」「じゃんけん」でヒットした以下のブログです。

ChatGPTも教えてくれない!? じゃんけんプログラムの作り方!
https://zenn.dev/nodamushi/articles/b3c4c9802e7f5e

ぜひ見ていただきたい。私には難しすぎる。

読んでもわかりません。。

しかし今はAIがあります。

【共有】サンプルページ

リアルタイム多人数じゃんけん。複数人でプレイ可能です。

複数人じゃんけんオンラインゲーム(サンプルページ)
https://astrowave.jp/amnesia_record/janken.php

特徴

  • 制限なしで何人でも同時プレイ可能
  • 遠く離れた人ともオンラインで対戦
  • サドンデス機能で決着まで自動進行
  • 見学モードで敗者も観戦可能

簡単操作・自動進行

  • ルーム作成はワンクリック
  • 共有URLで即座に参加
  • 全員選択完了で自動カウントダウン
  • サドンデス・あいこも自動で次ラウンド

使い方

1.ルームに参加

  • ホストから受け取った共有URLを開く
  • 自動的にルームに参加

2.手を選択

  • 自分のカード(「(あなた)」バッジ付き)で手を選ぶ
  • グー・チョキ・パーのいずれかを選択

3.結果確認

  • 全員選択完了まで待機
  • カウントダウン後、結果を確認

基本ルール

じゃんけんと同じです。

めんどくさいのは、共有URLを連絡する必要があること。

使用したものはPHPとJavascriptになります。

【共有2】試そうマニュアル

「新しいルームを作成」ボタンを押します。

するとルームが作成されます。

ゲームルームが作成されます

「緑」のコピーボタンを押して共有URLをコピーします。
共有URLをプレイしたい人に教えます。

URLをコピーして、じゃんけんしたい人に教えよう。

共有が確認できると、ホストと参加プレイヤーの内容は連動し始めます。

ゲーム開始でじゃんけんのモードに入ります。

何を出すか選ぼう。選んだら選択済みになります。

選んだ手は相手に見えません。

参加プレイヤーの手が出揃ったらカウントダウンが始まって勝敗が知らされます。

トロフィーのアイコンが付いた人の勝ち。

勝敗後もホストの「ゲーム開始」ボタンで何回もプレイ可能です。

人数が変わる時は、改めてルームを作成してください。
改める場合は、ホストのURLを再読み込みです。

【共有3】PHPファイルと構成

やりたい機能を追加していくと、とても長いコードになってしまいました。

ファイル構成

  • janken.php ← メインのじゃんけんアプリ
  • room_manager.php ← ルーム管理API
  • rooms.json  ← ルームデータ保存(自動生成)

興味のある人はページをダウンロードしてください。

janken.php

長すぎるので、掲載やめました。
ページごとダウンロードしてください。

636行
<!-- /////////////////////////////////////////////////////////////////////////
  ここから
//////////////////////////////////////////////////////////////////////////////-->

この間にコードあります。

<!-- /////////////////////////////////////////////////////////////////////////
  ここまで
//////////////////////////////////////////////////////////////////////////////-->
2006行まで

以下はルーム管理APIのphpファイル。
自分だけではとてもじゃないですが作れません。

room_manager.php

<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

// ルームデータファイルのパス
$roomsFile = 'rooms.json';

// ルームデータを読み込み
function loadRooms() {
    global $roomsFile;
    if (file_exists($roomsFile)) {
        $data = file_get_contents($roomsFile);
        $decoded = json_decode($data, true);
        return is_array($decoded) ? $decoded : [];
    }
    return [];
}

// ルームデータを保存
function saveRooms($rooms) {
    global $roomsFile;
    file_put_contents($roomsFile, json_encode($rooms, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE), LOCK_EX);
}

// 古いルームをクリーンアップ(設定可能な時間で経過したルームを削除)
function cleanupOldRooms(&$rooms) {
    $currentTime = time();
    // クリーンアップ時間設定(秒単位)
    // 1時間 = 3600秒, 6時間 = 21600秒, 24時間 = 86400秒
    $cleanupThreshold = 3600; // 1時間(必要に応じて調整)
    $timeThreshold = $currentTime - $cleanupThreshold;
    $removedCount = 0;
    
    foreach ($rooms as $roomId => $room) {
        if (isset($room['created_at']) && $room['created_at'] < $timeThreshold) {
            unset($rooms[$roomId]);
            $removedCount++;
        }
    }
    
    // クリーンアップ後にファイルを保存(古いルームを削除した場合のみ)
    if ($removedCount > 0) {
        saveRooms($rooms);
    }
    
    return $removedCount;
}

$method = $_SERVER['REQUEST_METHOD'];

if ($method === 'OPTIONS') {
    exit(0);
}

$rooms = loadRooms();
$removedCount = cleanupOldRooms($rooms);

// デバッグ用(本番環境では削除可能)
if ($removedCount > 0) {
    error_log("Cleaned up {$removedCount} old rooms");
}

switch ($method) {
    case 'GET':
        $action = $_GET['action'] ?? '';
        
        if ($action === 'get_room') {
            $roomId = $_GET['room_id'] ?? '';
            if (isset($rooms[$roomId])) {
                echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
            } else {
                echo json_encode(['success' => false, 'message' => 'Room not found']);
            }
        } elseif ($action === 'list_rooms') {
            echo json_encode(['success' => true, 'rooms' => $rooms]);
        }
        break;
        
    case 'POST':
        $input = json_decode(file_get_contents('php://input'), true) ?: [];
        $action = $input['action'] ?? '';
        
        if ($action === 'create_room') {
            $roomId = $input['room_id'] ?? '';
            $hostOnly = isset($input['host_only']) ? (bool)$input['host_only'] : false;
            $hostToken = bin2hex(random_bytes(16));
            if ($hostOnly) {
                $hostId = $input['host_id'] ?? '';
                $hostName = $input['host_name'] ?? '';
                $rooms[$roomId] = [
                    'id' => $roomId,
                    'host' => $hostId,
                    'host_name' => $hostName,
                    'host_token' => $hostToken,
                    'players' => [],
                    'gameState' => [
                        'phase' => 'waiting',
                        'selectedHands' => [],
                        'playerCount' => 0
                    ],
                    'created_at' => time()
                ];
            } else {
                // 従来: ホストも参加
                $playerId = $input['player_id'] ?? '';
                $playerName = $input['player_name'] ?? '';
                $rooms[$roomId] = [
                    'id' => $roomId,
                    'host' => $playerId,
                    'host_name' => $playerName,
                    'host_token' => $hostToken,
                    'players' => [[
                        'id' => $playerId,
                        'name' => $playerName,
                        'isHost' => true
                    ]],
                    'gameState' => [
                        'phase' => 'waiting',
                        'selectedHands' => [],
                        'playerCount' => 0
                    ],
                    'created_at' => time()
                ];
            }
            saveRooms($rooms);
            echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
            
        } elseif ($action === 'join_room') {
            $roomId = $input['room_id'] ?? '';
            $playerId = $input['player_id'] ?? '';
            $playerName = $input['player_name'] ?? '';
            if (isset($rooms[$roomId])) {
                // 既存プレイヤー重複チェック(playerIdで判定)
                $exists = false;
                foreach ($rooms[$roomId]['players'] as $p) {
                    if (isset($p['id']) && $p['id'] === $playerId) {
                        $exists = true;
                        break;
                    }
                }
                if (!$exists) {
                    $rooms[$roomId]['players'][] = [
                        'id' => $playerId,
                        'name' => $playerName,
                        'isHost' => false
                    ];
                    saveRooms($rooms);
                }
                echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
            } else {
                echo json_encode(['success' => false, 'message' => 'Room not found']);
            }
            
        } elseif ($action === 'update_game_state') {
            $roomId = $input['room_id'] ?? '';
            $gameState = $input['game_state'] ?? [];
            $hostToken = $input['host_token'] ?? '';
            if (!isset($rooms[$roomId])) { echo json_encode(['success'=>false,'message'=>'Room not found']); break; }
            if ($hostToken !== ($rooms[$roomId]['host_token'] ?? '')) { echo json_encode(['success'=>false,'message'=>'forbidden']); break; }
            $rooms[$roomId]['gameState'] = $gameState;
            saveRooms($rooms);
            echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
        } elseif ($action === 'select_hand') {
            $roomId = $input['room_id'] ?? '';
            $playerId = $input['player_id'] ?? '';
            $hand = $input['hand'] ?? '';

            if (!isset($rooms[$roomId])) { echo json_encode(['success'=>false,'message'=>'Room not found']); break; }
            // プレイヤーIDからインデックスを特定
            $players = $rooms[$roomId]['players'] ?? [];
            $playerIndex = -1;
            foreach ($players as $i => $p) {
                if (isset($p['id']) && $p['id'] === $playerId) { $playerIndex = $i; break; }
            }
            if ($playerIndex === -1) { echo json_encode(['success'=>false,'message'=>'player not in room']); break; }
            if (!isset($rooms[$roomId]['gameState'])) { $rooms[$roomId]['gameState'] = []; }
            if (!isset($rooms[$roomId]['gameState']['selectedHands'])) { $rooms[$roomId]['gameState']['selectedHands'] = []; }
            $rooms[$roomId]['gameState']['selectedHands'][(string)$playerIndex] = $hand;
            saveRooms($rooms);
            echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
        } elseif ($action === 'start_countdown') {
            $roomId = $input['room_id'] ?? '';
            $hostToken = $input['host_token'] ?? '';
            if (!isset($rooms[$roomId])) { echo json_encode(['success'=>false,'message'=>'Room not found']); break; }
            if ($hostToken !== ($rooms[$roomId]['host_token'] ?? '')) { echo json_encode(['success'=>false,'message'=>'forbidden']); break; }
            if (!isset($rooms[$roomId]['gameState'])) { $rooms[$roomId]['gameState'] = []; }
            $rooms[$roomId]['gameState']['phase'] = 'countdown';
            $rooms[$roomId]['gameState']['countdownAt'] = time();
            saveRooms($rooms);
            echo json_encode(['success' => true, 'room' => $rooms[$roomId]]);
        } elseif ($action === 'game_finished') {
            // ゲーム終了時にルームを削除(オプション)
            $roomId = $input['room_id'] ?? '';
            $hostToken = $input['host_token'] ?? '';
            if (!isset($rooms[$roomId])) { echo json_encode(['success'=>false,'message'=>'Room not found']); break; }
            if ($hostToken !== ($rooms[$roomId]['host_token'] ?? '')) { echo json_encode(['success'=>false,'message'=>'forbidden']); break; }
            
            // ルームを削除
            unset($rooms[$roomId]);
            saveRooms($rooms);
            echo json_encode(['success' => true, 'message' => 'Room deleted']);
        }
        break;
}

?>

セッション管理は以下

  • ブラウザのlocalStorageに依存
  • プライベートブラウジングでは動作しない場合がある

【ご注意】設置する場合のセキュリティ面など

プログラムは初心者のようなもの。何度も修正して、ようやく今の形になりました。

まだ変なところあるかも。。

AIに現在の動作環境と、ほか今後の改善点を聞きました。

APIエンドポイントの保護

  • room_manager.phpが直接アクセス可能になっている
  • 必要に応じて認証やレート制限を追加することを推奨

ホストトークンの管理

  • 現在は単純なハッシュ値を使用
  • より強固な認証システムへの移行を検討

依存関係

  • jQuery 3.6.3以上
  • モダンブラウザ(ES6対応)

サーバー環境

PHPの要件

  • PHP 7.0以上が必要
  • json、file_get_contents、file_put_contents関数が使用可能である必要

ファイル権限

  • rooms.jsonファイルの書き込み権限が必要
  • 適切なディレクトリ権限の設定

同時接続数

  • 現在はファイルベースの保存
  • 大量の同時アクセスには不向き
  • 必要に応じてデータベースへの移行を検討してください

【AI】イラストを描いてもらった

今回の記事のキャッチ画像で使わせてもらいます「Google ImageFX」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

この記事にピッタリなイラストのための考えたリクエストは、「サイバー空間でグーチョキパーのじゃんけんゲーム。突き出された手を上からの構図で、白熱の状況。全員女性で日本のアニメ風。」です。

このアプリでこんな風に白熱しますかね。

無理じゃないでしょうか。

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

どこかで聞いたことがあるような。

楽しげで、元気が出そうですね。