「Socket.IO ブラウザゲーム プログラム」ページで、「プレイヤーマッチングゲームの基本プログラム」を公開しています。
言語は TypeScript、サーバーは node.js で動作します。

Socket.IO ブラウザゲーム プログラム
https://bakuretuken.com/socket_game_base/
具体的にこの基本プログラムをつかってどの用に Scoket.IO ゲームを制作するのか、説明します。
昔作って公開していた「ワンナイト人狼 (*1)」の ゲーム開始時のカード配布処理 を例に説明します。
このページで作成したプログラムは、GitHub プロジェクトの「game_sample_01」ブランチに置いてあります。
game_sample_01 ブランチ
https://github.com/BakuretuKen/socket_game_base_room/tree/game_sample_01
ゲーム固有設定
「ワンナイト人狼」ゲームの設定を共通プログラム game_base.ts に記載します。
ルールについてはオフィシャルルールブックを参考にしてください。
・最小、最大ユーザー数
・役職定義
・人数別の役職デッキ(プレイヤー数 + 予備2枚)
■ game_base.ts
const minUserCount = 3; // ゲーム開始に必要な最小ユーザー数
const maxUserCount = 7; // 最大ユーザー数
+// 役職定義(全プレイヤー共通) key=役職ID
+const roles: { [key: number]: { name: string; code: string } } = {
+ 0: { name: "村人", code: "citizen" },
+ 1: { name: "占い師", code: "seer" },
+ 2: { name: "怪盗", code: "thief" },
+ 10: { name: "人狼", code: "werewolf" },
+};
+// 人数別の役職デッキ(プレイヤー数 + 予備2枚)
+const roleDecks: { [playerCount: number]: number[] } = {
+ 3: [0, 1, 2, 10, 10], // 村人、占い師、怪盗、人狼×2
+ 4: [0, 0, 1, 2, 10, 10], // 村人×2、占い師、怪盗、人狼×2
+ 5: [0, 0, 0, 1, 2, 10, 10], // 村人×3、占い師、怪盗、人狼×2
+ 6: [0, 0, 0, 0, 1, 2, 10, 10], // 村人×4、占い師、怪盗、人狼×2
+ 7: [0, 0, 0, 0, 1, 2, 10, 10, 10], // 村人×4、占い師、怪盗、人狼×3
+};
次に game_base.ts に「自分の情報(役職等)」と「プレイヤー全員の情報」を保持する変数を追加します。
let gameCode: string;
let isMaster: boolean;
let playerName: string;
+let player: {
+ roleId?: number; // 自分に配られた役職ID
+ playerIndex?: number; // players 配列内の自分の番号
+} = {};
let players: {
socketId: string;
userName: string;
+ roleId?: number; // 配布された役職ID(ゲーム開始後・マスターが保持)
}[] = [];
+let spareCards: number[] = []; // 予備カード2枚(マスターが保持)
プレイヤー全員の情報の同期
ゲーム開始時に「プレイヤー全員の情報」のマスターから各プレイヤーに送信します。startGame() 巻数を新規作成します。
■ game.ejs
<ol id="waitingRoomList"></ol>
<% if (master == "1") { %>
- <button id="startGameButton" disabled onclick="sendMessage({action: 'START_GAME'});">ゲーム開始</button>
+ <button id="startGameButton" disabled onclick="startGame();">ゲーム開始</button>
<% } %>
■ game_base.ts
+// ゲーム開始(マスター用・roleId を除いた players を全員に送る)
+function startGame(): void {
+ sendMessage({
+ action: "START_GAME",
+ players: players.map(function(p) {
+ return {
+ socketId: p.socketId,
+ userName: p.userName
+ };
+ })
+ });
+}
送信された「START_GAME」の受信部を作ります。
まずはマスターの処理から
■ game_master.ts
// 受信処理(ここを編集してゲーム作成)
(function() {
window.receiveMessage = function(arr: RecvMessage): void {
debugLog("recv(master):", arr);
switch (arr.action) {
case "START_GAME":
+ assignRoles();
// ゲーム開始したのでサーバーからゲームコードを削除(以降の新規参加を締め切る)
socket.emit("delete_code", { gameCode: gameCode });
hideMessages();
document.getElementById("waitingRoom")!.style.display = "none";
document.getElementById("gameScreen")!.style.display = "block";
document.getElementById("debugArea")!.style.display = "block";
+ showGameCards();
配役を全員に配布する assignRoles() と、画面にカードを表示する showGameCards() は後で作成します。
game_player.ts では、受け取った players 情報を players 変数に保持します。
■ game_player.ts
// 受信処理(ここを編集してゲーム作成)
(function() {
window.receiveMessage = function(arr: RecvMessage): void {
debugLog("recv(player):", arr);
switch (arr.action) {
case "START_GAME":
+ // マスターから送られた全プレイヤー情報を保持(roleId は含まれない)
+ if (arr.players) {
+ players = arr.players;
+ }
hideMessages();
document.getElementById("waitingRoom")!.style.display = "none";
document.getElementById("gameScreen")!.style.display = "block";
document.getElementById("debugArea")!.style.display = "block";
+ showGameCards();
+ break;
}
};
カードのシャフルと配布
人数に応じてカードを用意し、シャフルする処理を作成します。assignRoles() 関数。
その後、sendDirectMessage で個別にプレイヤーに役職とそのプレイヤーの配列番号を配ります(ASSIGN_ROLE action)
■ game_master.ts
+// デッキをシャッフルして各プレイヤーに役職を配布し、余り2枚を予備カードに保持
+function assignRoles(): void {
+ const deckTemplate = roleDecks[players.length];
+ if (!deckTemplate) {
+ console.error("未対応の人数です: " + players.length);
+ return;
+ }
+ // コピーしてシャッフル(Fisher-Yates)
+ const deck = deckTemplate.slice();
+ for (let i = deck.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ const tmp = deck[i];
+ deck[i] = deck[j];
+ deck[j] = tmp;
+ }
+ // 先頭からプレイヤー人数分を配布
+ for (let i = 0; i < players.length; i++) {
+ players[i].roleId = deck[i];
+ }
+ // 残り2枚を順番付きで予備カードに保持
+ spareCards = deck.slice(players.length);
+ debugLog("assignRoles players:", players);
+ debugLog("assignRoles spareCards:", spareCards);
+
+ // 各プレイヤーに自分の役職と配列番号を通知
+ for (let i = 0; i < players.length; i++) {
+ sendDirectMessage(players[i].socketId, {
+ action: "ASSIGN_ROLE",
+ roleId: players[i].roleId,
+ playerIndex: i
+ });
+ }
+}
送信した ASSIGN_ROLE を取得するコードを書きます。
■ game_master.ts
// 受信処理(ここを編集してゲーム作成)
(function() {
window.receiveMessage = function(arr: RecvMessage): void {
debugLog("recv(master):", arr);
switch (arr.action) {
・・・
+ case "ASSIGN_ROLE":
+ player.roleId = arr.roleId;
+ player.playerIndex = arr.playerIndex;
+ console.log("roleId:", player.roleId, "playerIndex:", player.playerIndex);
+ showGameCards(); // カード表示
break;
}
};
■ game_player.ts
// 受信処理(ここを編集してゲーム作成)
(function() {
window.receiveMessage = function(arr: RecvMessage): void {
console.log("recv(player):", arr);
・・・
+ case "ASSIGN_ROLE":
+ player.roleId = arr.roleId;
+ player.playerIndex = arr.playerIndex;
+ console.log("roleId:", player.roleId, "playerIndex:", player.playerIndex);
+ showGameCards(); // カード表示
break;
}
};
カードの画面表示
カードを画面に表示する関数 showGameCards() を作成していきます。
予備カード2枚と各プレイヤーのカードをプレイヤーの名前付きで表示します。
全プレイヤーの名前は players に入っています(START_GAME で送信されてきた)。
自分のカード表示だけ、名前の右に役職を表示します。
役職のIDは、player.roleId に保持されています(ASSIGN_ROLE で送信されてきた)。
players 内の自分の位置は player.playerIndex で判断できます(ASSIGN_ROLE で送信されてきた)。
画面表示処理は、マスター、プレイヤー同じなので共有処理 game_base.ts に記載します。
■ game_base.ts
+// ゲーム画面にカードを裏面で表示(予備2枚+プレイヤー分)
+function showGameCards(): void {
+ const spareArea = document.getElementById("spareCardsArea");
+ const playerArea = document.getElementById("playerCardsArea");
+ if (!spareArea || !playerArea) {
+ return;
+ }
+
+ // 予備カード2枚を横並び
+ spareArea.innerHTML = "";
+ for (let i = 0; i < 2; i++) {
+ const card = document.createElement("div");
+ card.className = "card";
+ card.textContent = "■";
+ spareArea.appendChild(card);
+ }
+
+ // プレイヤー分のカードを縦並び(右に名前・自分のみ役職も表示)
+ playerArea.innerHTML = "";
+ players.forEach(function(p, index) {
+ const row = document.createElement("div");
+ row.className = "player-card-row";
+
+ const card = document.createElement("div");
+ card.className = "card";
+ card.textContent = "■";
+
+ const name = document.createElement("span");
+ name.className = "player-name";
+ let nameText = p.userName;
+ // 自分のカードのみ役職を名前の右に表示
+ if (player.playerIndex === index && player.roleId !== undefined && roles[player.roleId]) {
+ nameText += "(" + roles[player.roleId].name + ")";
+ }
+ name.textContent = nameText;
+
+ row.appendChild(card);
+ row.appendChild(name);
+ playerArea.appendChild(row);
+ });
+}
ケームの開始処理の一部ですが、ひとまず「カードをシャフルして、プレイヤーに配る」ところまで、完成です。
実行確認

カードが配られて、自分の画面だけ自分の役職が表示されています。
今回は「怪盗」がカード交換した際や、「占い師がカードを占った」ときは、sendDirectMessage でマスターに問い合わせる処理を考えて作りました(未実装ですが)。
どこまでマスターに問い合わせるかは、いろいろ考えられると思います。
例えば、ASSIGN_ROLE で自分のみの役職ではなく、全ユーザーの役職と残りカード情報を送れば、少なくても「占い師がカードを占った」ときはマスターへの通信が不要になります。
画面にカード画像を表示する際は、役職定義で code を持っているので、
- citizen.png
- seer.png
- thief.png
- werewolf.png
を用意して表示すると、画像ファイル管理がわかりやすいと思います。
見ての通り、この「基本プログラム」でゲームを作成する場合、
基本的には sendMessage、sendDirectメッセージで ASSIGN_ROLE のようなアクションを送信します。
受信側で受け取ったアクション毎の処理(swtich文)を書いて開発を進めていくスタイルです。
[マスター] - message → [プレイヤー(マスター含む)]
プレイヤーの投票などのアクションは、逆向きのメッセージ送信となります。
マスターのみへの送信は players[0].soccketid へ、sendDirectメッセージで行えます。
[プレイヤー(マスター含む)] - message → [マスター]
(*1) ワンナイト人狼は、オオカミさん氏作成のカードゲーム人狼を簡単にしたゲームです。オフィシャルWEBサイト