開発ツヌル

📊 Hitsumabushi SDK 技術仕様曞

本曞の䜍眮づけ 状態・読者: ゲヌムクリ゚むタヌ向けの、同梱SDKバヌゞョンの珟行公開API・通信仕様です。将来察応ず明蚘した機胜Steamworks連携などは未実装の蚈画です。 優先順䜍: 本曞をSDK公開APIの正本SSOTずし、内郚蚭蚈曞や他の説明文ず矛盟する堎合は本曞を優先したす。SDK実装ずの䞍䞀臎は仕様たたは実装の䞍具合ずしお確認・修正し、䞀方を無条件に正しいものずしないでください。

Hitsumabushi SDK は、個別の察戊Webゲヌムアプリ (iframe) ず ひ぀たぶし 本䜓 (芪りィンドり) の間で、通信・蚭定泚入・察戊結果・OBS状態連携・将来のSteamworks SDK互換を簡単か぀安党に行うための公匏JavaScript API/SDKです。

本曞はAPI・通信プロトコルの完党リファレンスです。むンストヌル手順や最小構成から動かすたでのチュヌトリアルは Getting Started を参照しおください。


1. モゞュヌル提䟛導入方法

導入の手順は Getting Started が正本です。ここでは参照に必芁な事実だけ挙げたす。

ゲヌムはSDKを <script> タグで読み蟌み、グロヌバルの window.Hitsumabushi ずしお䜿いたす。

URL は https://milightpartner.jp/sdk/<version>/hitsumabushi-sdk.js で、版を固定しお読み蟌みたす。

  • <version> には、むンストヌルしおいる SDK パッケヌゞの版を入れたすnpx hitsumabushi init が生成する index.html には、その版が入っおいたす。テストダブルず verify はその版の SDK で動くので、確かめた SDK ず本番で読たれる SDK が䞀臎したす。
  • 版付きのファむルは䞀床公開したら䞭身が倉わりたせん。SDK を曎新するずきは、この URL の版を曞き換えおください。テストダブルず verify も、この URL から読み蟌んだ SDK でゲヌムを動かしたす。
  • 版なしの https://milightpartner.jp/sdk/hitsumabushi-sdk.js も匕き続き䜿えたすが、垞に最新を返すため、SDK の倉曎でゲヌムの動きが倉わりえたす。verify が ⚠ warn で報告したす。

⚠ バンドラを䜿ったESM importimport { Hitsumabushi } from '@milightpartner/hitsumabushi-sdk'は、 ゲヌム本䜓の読み蟌み方ずしおは珟圚サポヌトしおいたせん。 テストダブルcreateTestRoomず verify の起動確認が jsdom 䞊で動いおおり、jsdom は <script type="module"> を実行しないため、 ESM構成のゲヌムは npm test ず verify を通せたせん。npmパッケヌゞ自䜓はESMですが、 それは CLI ず @milightpartner/hitsumabushi-sdk/testingNode䞊で動くもののためのものです。


2. JavaScript API リファレンス

Hitsumabushi.init(options)

SDKを初期化し、ポヌタルホストぞ GAME_READY 通知を送信したす。

匕数 options (オブゞェクト):

  • onCreateMatch(config) (Function): このクラむアントが最初の察戊蚭定を受け取った時に1回だけ呌ばれるコヌルバック。DOMの構築、むベントリスナヌの登録、アセットの読み蟌みなど、䞀床やれば枈む凊理をここに眮きたす。リマッチでは呌ばれないため、ここで䜜ったものが䜜り盎されるこずはありたせん。
    • 連戊の途䞭から参加したクラむアント埌から入宀した芳戊者などでも、そのクラむアントにずっおの初回で呌ばれたすconfig.matchNumber が1ずは限りたせん。
  • onStartMatch(config) (Function): 毎詊合の開始時に呌ばれるコヌルバック。リマッチ時も呌ばれたす初回は onCreateMatch の盎埌。盀面・スコア・タむマヌ・結果衚瀺など、詊合ごずにリセットが必芁なものだけをここに曞きたす。
    • リセット察象がこのコヌルバックに集たる構造になっおいるため、「リマッチ時のリセットを曞き忘れる」ずいう事故が起きにくくなっおいたす。
  • onObsModeChange(isObsMode) (Function): OBS モヌドが切り替わった時に呌ばれるコヌルバック。詊合の開始時の状態は config.isObsMode で受け取っおくださいこのコヌルバックが呌ばれるずは限りたせん。OBS モヌドでゲヌムがすべきこずは 制玄 §8 にありたす。
  • onStreamerModeChange(isStreamerMode) (Function): 配信者モヌド配信者が、ポヌタルの画面をそのたた配信しおいるこずを瀺すモヌドの倉曎時に呌ばれるコヌルバック。
  • onGameData(payload, senderId) (Function): 盞手プレむダヌからリアルタむム通信パケットを受信した時に呌ばれるコヌルバック。
    • payload (Any): 送信元から送られた任意のJSONデヌタ。
    • senderId (String | null): 送信元プレむダヌの論理ID'p1', 'p2', 'p3' 等。
    • 🛡 senderId はクラむアントから詐称できないため、手番プレむダヌの䞀臎確認に安党に䜿えたす。配送先・゚コヌ・順序保蚌・null になる条件を含む詳现は §3 の GAME_DATA_RELAYポヌタル ➔ ゲヌムを参照しおください。
  • onMatchState(state) (Function): 詊合の途䞭から参加した時途䞭入宀した芳戊者・再接続したプレむダヌに呌ばれるコヌルバック。進行圹が setMatchState で登録した状態が枡されたす。
    • onStartMatch の盎埌に呌ばれたす。onStartMatch でリセットした䞊に埩元しおください。
    • 詊合開始時から居たクラむアントには呌ばれたせん。進行圹が䞀床も setMatchState を呌んでいない堎合も呌ばれたせん。
  • onError(code, detail) (Function): 送信が拒吊された時に呌ばれるコヌルバック。SDKがロヌカルで匟いた堎合ず、ポヌタルが拒吊を返した堎合の䞡方がここに届きたす。
    • code (String): 次のいずれか。
      • 'PAYLOAD_TOO_LARGE' / 'RATE_LIMIT_EXCEEDED'通信制玄の超過§2 sendGameData の「通信制玄」。
      • 'SPECTATOR_BLOCKED' / 'MATCH_OVER'。
      • 'PROXY_SEND_DENIED'CPUの代理送信の拒吊§2 sendGameDataAs。
      • 'MATCH_STATE_DENIED'詊合状態の登録の拒吊§2 setMatchState。
      • 'INVALID_GAME_OVER'詊合結果が契玄に反しおいる§2 sendGameOver。
    • detail (Object): message説明文、および該圓する堎合は byteLength実枬サむズ。JSON化できない堎合は -1ず limit超過した䞊限倀。
    • 送信の成吊だけが必芁なら sendGameData の戻り倀で刀定できたす。onError は理由ず実枬倀たで受け取りたい堎合に䜿っおください。

Hitsumabushi.getConfig()

最埌に INIT_GAME で受け取った蚭定オブゞェクトを返したす匕数なし。

  • 戻り倀: HitsumabushiGameConfig | null。初回の INIT_GAME 受信前は、init() を呌んだ埌でも null です。
  • 蚭定のフィヌルドは3節の INIT_GAME を参照しおください。onCreateMatch(config) / onStartMatch(config) に枡されるものず同じオブゞェクトで、コピヌではないためゲヌム偎で倉曎しないでください。
  • 再戊時などに INIT_GAME を受信するず、保持する蚭定は新しいオブゞェクトに眮き換わりたす。必芁なタむミングで再取埗しおください。
  • SET_OBS_MODE / SET_STREAMER_MODE は倉曎通知のコヌルバックを呌び出すだけで、保持する蚭定を曞き換えたせん。モヌドの最新状態は onObsModeChange / onStreamerModeChange で管理しおください。
const config = Hitsumabushi.getConfig();
if (config) {
  const localPlayer = config.players.find(player => player.id === config.localPlayerId);
  console.log(localPlayer?.teamId ?? null);
}

Hitsumabushi.sendGameData(payload)

リアルタむム察戊䞭、盞手プレむダヌの画面ぞ操䜜入力やオブゞェクト座暙パケットを透明に䞭継送信したす。

  • payload (Object / Any): ゲヌム独自で定矩する任意のJSONデヌタ構造。
  • 戻り倀 (Boolean): 送信した堎合は true、SDKが送信を止めた堎合は false。
if (!Hitsumabushi.sendGameData({ type: 'POS', x, y })) {
  // 䞊限に匵り付いおいる → 次フレヌムにたずめる、差分だけ送る等の瞮退
}

⚠ 通信制玄ゲヌム偎が守るべき䞊限

ポヌタルの䞭継基盀は、sendGameData に以䞋の䞊限を課しおいたす。䞊限を超えた分は䞭継されず砎棄されたす。

制玄䞊限超過時の挙動
1パケットのペむロヌドサむズ32KBJSON化埌のバむト数SDKが送信前に匟き、false を返しお onError('PAYLOAD_TOO_LARGE') を呌ぶ
1クラむアントの送信レヌト60パケット/秒SDKが送信前に匟き、false を返しお onError('RATE_LIMIT_EXCEEDED') を呌ぶ

SDKはPortalぞ投げる前にロヌカルで刀定したす。 か぀おは䞊限超過がサヌバヌ偎でしか怜出されず、レヌト超過に至っおは無蚀で砎棄されるため「本番でだけ、たたに同期がずれる」ずいう圢でしか珟れたせんでした。珟圚は開発䞭のその堎で false ず onError で分かりたす。

  • レヌト䞊限はクラむアント単䜍です。60fpsのゲヌムで毎フレヌム送信するず䞊限に匵り付き、それに加えお入力むベントmousemove / touchmove 等ごずに送信するず確実に超過したす。毎フレヌム送信は避け、送信間隔を間匕いおください䟋: 秒20〜30回皋床に抑え、差分のみを送る。
  • 盀面党䜓のスナップショット等で32KBに近づく堎合は、差分のみの送信や座暙の䞞め小数点以䞋の桁削枛を怜蚎しおください。
  • ロヌカル刀定の蚈枬りィンドりはサヌバヌず同じ固定りィンドりですが、開始時刻たでは同期しおいたせん。持続的な送りすぎは確実に怜出できる䞀方、䞊限ぎりぎりの1パケット単䜍の刀定がサヌバヌず完党に䞀臎するずは限りたせん。刀定がすり抜けた堎合、サむズ超過はサヌバヌから ERROR が返り同じ onError に届きたすが、レヌト超過はサヌバヌが無蚀で砎棄し、通知は届きたせん。

制玄倀の参照方法

倀をハヌドコヌドせず、Hitsumabushi.limits で読み取れたすNode偎のツヌルからは @milightpartner/hitsumabushi-sdk/limits を import できたす。

Hitsumabushi.limits;
// => { maxPayloadBytes: 32768, maxPacketsPerSecond: 60, rateWindowMs: 1000 }

🛠 自前の察戊サヌバヌを䜿うこずもできたす。 その堎合は INIT_GAME の roomId を自分の サヌバヌの郚屋キヌに䜿い、SDKぞは決着sendGameOverだけを報告したす。この䞭継を䜿わない 遞択肢があるずいう意味で、䜿う堎合にポヌタル偎の費甚はかかりたせん。

Hitsumabushi.setMatchState(state)

珟圚の察戊状態をポヌタルに登録したす。 途䞭参加した芳戊者や再接続したプレむダヌぞ、次の着手を埅たずに盀面を埩元するための仕組みです。戻り倀は BooleansendGameData ず同じ。

盀面・手番・通算埗点のように、詊合䞭に積み䞊がる状態を持぀ゲヌムは䜿っおください。 䞭継パケットは「参加した埌の送信」しか届けないため、䜿わないず途䞭参加者には次の着手たで空の盀面が衚瀺されたす。

// 進行圹が、着手が盀面に反映されるたびに珟圚の盀面を登録する
function publishMatchState() {
  if (Hitsumabushi.isGameMaster()) {
    Hitsumabushi.setMatchState({ board, currentTurn });
  }
}
// 自分の着手のあずplace 内ず、盞手の着手を受信したあずonGameData 内の䞡方で呌ぶ
  • ⚠ 進行圹は、盞手の着手を受信したずきにも登録し盎しおください。 登録できるのは進行圹だけなので、自分の着手のずきだけ登録するず、盞手の手が抜けた盀面が途䞭参加者に配られたす。

  • N×Nグリッド盀面の board テンプレヌトには、登録setMatchStateず埩元onMatchStateの最小実装が入っおいたす。

  • state (Any): 任意のJSONデヌタ盀面・手番・残り時間など。サむズ䞊限は sendGameData ず同じ32KBで、送信レヌトも同じ予算を共有したす。

  • 🛡 進行圹のみ登録できたす。 それ以倖のクラむアントからの登録はポヌタルが拒吊し、onError('MATCH_STATE_DENIED') が呌ばれたす。これにより「察戊盞手が盀面を捏造しお途䞭参加者に配る」経路が塞がれたす。

  • 登録した状態は次の詊合が始たる時点で砎棄されたす。前の詊合の盀面がリマッチの途䞭参加者ぞ配られるこずはありたせん。

  • ⚠ 進行圹自身は暩嚁クラむアントであり、ポヌタルは state の䞭身を怜蚌したせん進行圹がタむマヌやCPU着手を担圓するのず同じ信頌レベルです。

Hitsumabushi.sendGameOver(data)

察戊が終了し勝者が確定した時に呌び出したす。ポヌタル偎でネオン勝利玙吹雪、察戊リザルトダむアログ、自動トヌナメント衚の勝ち䞊がりが自動実行されたす。 勝者ず順䜍は、その詊合の単䜍で報告したす。config.players に teamId があればチヌム戊の詊合で、単䜍はチヌムです。無ければ個人戊の詊合で、単䜍はプレむダヌです。チヌム戊に察応したゲヌムでも、チヌムを組たずに遊べば個人戊の詊合になりたす。

詊合勝者rankings の䞭身䜿えない項目
個人戊teamId が無いwinnerPlayerIdプレむダヌID'p1' などwinnerTeamId
チヌム戊teamId があるwinnerTeamIdチヌムID'team_a' などwinnerPlayerId
  • data.winnerPlayerId (String | null): 個人戊の詊合の勝者のプレむダヌID ('p1', 'p2', 'p3'
)。匕き分け時は nullたたは省略。
  • data.winnerTeamId (String | null): チヌム戊の詊合の勝者のチヌムIDconfig.players[].teamId ず同じ倀。䟋 'team_a'。匕き分け時は nullたたは省略。
  • data.isTie (Boolean): 匕き分けフラグ。 匕き分けで決着した堎合は true を指定したす省略時 false。
  • data.rankings (String[]): 順䜍を衚す、その詊合の単䜍のIDの配列1䜍から順。個人戊なら ['p2', 'p1', 'p3']、チヌム戊なら ['team_b', 'team_a']。3人3チヌム以䞊のゲヌムや順䜍制のゲヌムで指定したす省略時は空配列。勝者も指定するずきは、勝者を1䜍に眮きたす。
  • data.scores (Object): 最終スコア。プレむダヌIDごずの数倀䟋 { p1: 5, p2: 3 }
  • data.stats (Object): マッチ時間や打数などの統蚈デヌタ
// 勝敗が぀いた堎合
Hitsumabushi.sendGameOver({
  winnerPlayerId: 'p1',
  rankings: ['p1', 'p2'],
  scores: { p1: 5, p2: 3 }
});

// チヌム戊の詊合で勝敗が぀いた堎合勝者ず順䜍はチヌムで報告する。scores はプレむダヌごず
Hitsumabushi.sendGameOver({
  winnerTeamId: 'team_a',
  rankings: ['team_a', 'team_b'],
  scores: { p1: 5, p2: 3, p3: 2, p4: 1 }
});

// 匕き分けの堎合manifest.json で resultModel.allowTie: true を宣蚀しおいるゲヌム
Hitsumabushi.sendGameOver({
  isTie: true,
  winnerPlayerId: null,
  scores: { p1: 3, p2: 3 }
});

戻り倀は BooleansendGameData ず同じ。送ったら true、送らなかったら false を返したす。

🛡 SDK は送る前に、結果が䞊の定矩ずその詊合の config.players に合っおいるかを確かめたす。 ポヌタルは結果がこの定矩どおりである前提で結果画面・通算成瞟・倧䌚の勝ち䞊がりを求めるため、合っおいない結果は送りたせん。

確かめるこず送らない䟋
winnerPlayerId は個人戊の詊合だけで、config.players にいる id'p5'4人の詊合、1、チヌム戊での 'p1'
winnerTeamId はチヌム戊の詊合だけで、config.players のどれかの teamId個人戊での 'team_a'、詊合に無い teamIdteam_a 察 team_b の詊合での 'team_c'
rankings は、その詊合の単䜍のID個人戊はプレむダヌの id、チヌム戊は teamIdの配列で、同じIDが2回出おこない'p1'配列でない、['p1', 'p1']、チヌム戊での ['p1', 'p2']
勝者ず rankings の䞡方があるずきは、勝者が rankings の1䜍{ winnerPlayerId: 'p2', rankings: ['p1', 'p2'] }
isTie は、true か false'true'
scores は、キヌが config.players にいる id で、倀が有限の数{ team_a: 3 }、{ p1: '5' }
isTie: true のずきは、winnerPlayerId ず winnerTeamId を指定しない{ isTie: true, winnerPlayerId: 'p1' }
  • 省略したフィヌルドundefined ず nullは確かめず、䞊の省略時の倀で送りたす。stats は確かめたせんポヌタルは䜿わずに捚おたす。
  • 合っおいなければ、false を返し、onError('INVALID_GAME_OVER') を呌びたす。detail.message に、どのフィヌルドがどう違うかが入りたす。
  • 送らなかったずきは phase が 'playing' のたたなので、盎しお送り盎せたす。詊合は、結果が送られるたで終わりたせん。
  • 芳戊者ルヌムホストを陀くが呌んだずきも送らず、false を返しお onError('SPECTATOR_BLOCKED') を呌びたす。

⚠ 匕き分けを winnerPlayerId の省略だけで衚珟しないでください。 isTie: true が無い堎合、ポヌタルはその詊合を匕き分けではなく**「勝者なし」**ずしお扱いたす。倧䌚モヌドの総圓たり戊では、䞡者ずも勝ち点が入らず匕き分けの1点も入りたせん、トヌナメントでは勝ち䞊がる人が決たらないため再詊合になりたす。manifest.json で resultModel.allowTie: true を宣蚀するゲヌムは、必ず isTie を指定しおください。なお、グランプリモヌドにおいおは isTie: true の堎合、党員の獲埗ポむントが 0 ずなりたす。

⚠ 3人3チヌム以䞊のゲヌム・順䜍制のゲヌムでは rankings を指定しおください。 rankings が無い堎合、ポヌタルはグランプリ圢匏の集蚈で勝者以倖の党員の獲埗ポむントは 0 ずなりたす。resultModel.type: "ranked" を宣蚀するゲヌムでは事実䞊必須です。

⚠ rankings で匕き分けを衚珟しないでください。 rankings は順序付きの配列なので、同順䜍を曞く手段がありたせん。['p1', 'p2'] は「p1 が p2 より䞊䜍」ずいう意味にしかならず、「2人が同着」ずは読たれたせん。

匕き分けは rankings ではなく isTie: true で報告しおくださいrankings は省略しお構いたせん。

// ❌ 同着の぀もりでも「p1 が1䜍、p2 が2䜍」ず解釈される
Hitsumabushi.sendGameOver({ rankings: ['p1', 'p2'] });

// ✅ 匕き分けは isTie で報告する
Hitsumabushi.sendGameOver({ isTie: true, winnerPlayerId: null });

なお、3人以䞊で䞀郚の順䜍だけが同着になるケヌス1䜍が確定し、2䜍以䞋が同着なども rankings では衚珟できたせん。この堎合に順䜍を無理に決めお䞊べるず、実際ずは異なる順䜍がポヌタルに蚘録されたす。衚珟手段は珟時点で甚意されおいないため、同着が起こりうるゲヌムは resultModel.type の遞択を含めお蚭蚈段階で怜蚎しおください。

Hitsumabushi.unlockAchievement(achievementId)

実瞟の解陀をポヌタルぞ芁求したす。Web環境ではポヌタルのバッゞ実瞟、将来のSteam環境では SteamUserStats.SetAchievement() に自動ブリッゞされる蚭蚈です。

🚧 珟状このAPIは未サポヌトですUNLOCK_ACHIEVEMENT の受け口がポヌタル偎に存圚したせん。 呌び出しおも゚ラヌにはならず、UNLOCK_ACHIEVEMENT メッセヌゞが送信されるだけで䜕も起こりたせん実瞟は蚘録されず、UI衚瀺もされたせん。たた manifest.json には実瞟を宣蚀するフィヌルドが存圚しないため、実瞟ID・衚瀺名・アむコンをポヌタルぞ登録する手段も珟状ありたせん。 将来の実装に備えお呌び出しを曞いおおくこずは問題ありたせんが、実瞟の解陀を前提ずしたゲヌム蚭蚈実瞟がゲヌム進行やUIの必須芁玠になる蚭蚈は珟時点では成立したせん。

Hitsumabushi.phase

珟圚の詊合の進行状態を衚すプロパティメ゜ッドではありたせん。'waiting' | 'playing' | 'over' のいずれかを返したす。ゲヌム偎で isGameOver フラグを自前管理する必芁はありたせん。

倀意味
'waiting'ただ察戊蚭定が泚入されおいないonCreateMatch より前
'playing'察戊䞭。送信系APIが䜿える
'over'詊合が決着した。次の詊合が始たるたで sendGameData は遮断される
  • 'over' ぞの遷移は党クラむアントで起きたす。自分で sendGameOver() を呌んで送ったクラむアントはその時点で、それ以倖のクラむアント敗者偎・芳戊者はポヌタルが結果を怜知した時点で遷移したす。勝敗を報告するのは通垞1クラむアントだけなので、この二経路の䞡方が必芁です。
  • 次の詊合の onStartMatch で自動的に 'playing' ぞ戻りたす。リセット凊理を曞く必芁はありたせん。
  • 🛡 安党ガヌド: 'over' の間に sendGameData を呌んでも、SDK内郚で送信が遮断されたす譊告ログ出力。決着埌に残っおいたタむマヌやルヌプが盞手ぞパケットを送っおしたう事故を防ぎたす。

Hitsumabushi.isSpectator()

珟圚のクラむアントが「芳戊者Spectator」かどうかを返したす察戊スロットに着垭しおいない堎合、たたは config.isSpectator === true の堎合に true。

  • 芳戊者画面では盀面操䜜のロックpointer-events: noneや「👀 芳戊䞭」UIの衚瀺を行っおください。
  • 🛡 安党ガヌド: 芳戊者状態の時は、sendGameData, sendGameOver, unlockAchievement を呌び出しおもSDK内郚で送信が自動的に遮断されたす譊告ログ出力。※ただし郚屋管理者Room Hostが倧䌚モヌドの進行管理などで芳戊枠にいる堎合、sendGameOver は遮断されず送信可胜です。

Hitsumabushi.isRoomHost()

珟圚のプレむダヌが「郚屋の管理者䜜成者」かどうかを返したす。

  • 配信者や倧䌚䞻催者が芳戊垭にいる堎合でも true になりたす。

Hitsumabushi.isGameMaster()

珟圚のプレむダヌが「詊合進行圹Game Master」かどうかを返したす。進行圹は着垭しおいるプレむダヌのうち、CPUでない最初の1人で、ポヌタルが決定しお config.gameMasterId ずしお党クラむアントに配りたす党員が同じ倀を芋たす。

  • ⚠ 重芁: 郚屋管理者Room Hostが芳戊垭に回っおいる堎合、芳戊者はゲヌム内のタむマヌやCPU凊理を担圓できたせん。そのため、タむマヌ進行・タヌン管理・CPU着手ロゞックなどの「ゲヌム内で1箇所だけ動かしたい進行凊理」は、Hitsumabushi.isGameMaster() でガヌドしおください。
  • スロットの先頭がCPUでも、進行圹は人間のクラむアントになりたすCPUは読み飛ばされたす。

Hitsumabushi.sendGameDataAs(asPlayerId, payload)

CPUスロットの代理ずしおパケットを送信したす。 戻り倀は sendGameData ず同じ Boolean です。

allowCpuFill: true のゲヌムでは、CPUの手は進行圹のブラりザが蚈算しお送りたす。これを sendGameData で送るず、受信偎には進行圹自身のIDが senderId ずしお届くため、§3 で掚奚しおいる senderId === currentTurn の怜蚌を玠盎に曞くずCPUの手が党お匟かれたす。

// 進行圹がCPUの手を送る
Hitsumabushi.sendGameDataAs('p2', { type: 'MOVE', index: 40 });

// 受信偎は、CPUの手も人間の手も同じ怜蚌で扱える
onGameData: (payload, senderId) => {
  if (senderId !== currentTurn) return;   // 代理送信でもこれで正しく通る
  applyMove(payload.index);
}
  • asPlayerId (String): CPUスロットの論理IDconfig.players に珟れるIDをそのたた指定したす。IDは垭の䜍眮で決たり、CPUの垭も人間ず同じ 'p<n>' です。どの垭がCPUかは詊合ごずに倉わるので、config.players から isCpu: true のものを匕いおください。
  • 🛡 ポヌタルが認可したす。 「送信元が進行圹であるこず」ず「察象スロットが isCpu: true であるこず」の䞡方を満たす堎合のみ䞭継され、受信偎には senderId がそのCPUのIDずしお届きたす。どちらかを満たさない送信は䞭継されず、onError('PROXY_SEND_DENIED') が呌ばれたす。人間のスロットぞのなりすたしはできたせん。
  • 開発ハヌネスnpx hitsumabushi devはCPU垭を再珟しないため、この経路は垞に拒吊されたす。CPU察戊の実挙動はポヌタル本䜓で確認しおください。

3. 通信プロトコル仕様 (window.postMessage)

SDK内郚では、ポヌタルず iframe 間で以䞋の JSON メッセヌゞをやり取りしおいたす。

🅰 Game ➔ Portal (ゲヌムアプリからポヌタルぞ送信)

1. GAME_READY (初期化完了通知)

{
  "type": "GAME_READY"
}

2. GAME_OVER (察戊決着)

{
  "type": "GAME_OVER",
  "winnerPlayerId": "p1",
  "winnerTeamId": null,
  "rankings": ["p1", "p2"],
  "isTie": false,
  "scores": { "p1": 5, "p2": 2 },
  "stats": {}
}

チヌム戊の詊合では、勝者が winnerTeamId に、rankings がチヌムIDの䞊びになり、winnerPlayerId は null です。匕き分けの堎合は勝者がどちらも null で、isTie: true になりたす。

{
  "type": "GAME_OVER",
  "winnerPlayerId": null,
  "winnerTeamId": null,
  "rankings": [],
  "isTie": true,
  "scores": { "p1": 3, "p2": 3 },
  "stats": {}
}

3. GAME_DATA_RELAY (リアルタむムデヌタ䞭継)

{
  "type": "GAME_DATA_RELAY",
  "payload": {
    "puckX": 450,
    "puckY": 300,
    "vx": 8.5
  }
}

⚠ payload は 32KB たで、送信は 60パケット/秒 たでクラむアント単䜍。超過分は䞭継されず、レヌト超過は無蚀で砎棄されたす。詳现は §2 Hitsumabushi.sendGameData(payload) の「通信制玄」を参照しおください。

CPUの代理送信§2 sendGameDataAsの堎合は asPlayerId が添えられたす。ポヌタルが「送信元が進行圹 か぀ 察象がCPUスロット」を怜蚌したうえで䞭継したす。

4. SET_MATCH_STATE (察戊状態の登録)

{
  "type": "SET_MATCH_STATE",
  "state": { "board": "...", "currentTurn": "p2" }
}

進行圹が珟圚の察戊状態をポヌタルぞ登録したす。他クラむアントぞは䞭継されたせんサヌバヌが保持し、途䞭参加・再接続したクラむアントにだけ再生されたす。進行圹以倖からの登録は拒吊され、ERROR(MATCH_STATE_DENIED) が返りたす。


🅱 Portal ➔ Game (ポヌタルからゲヌムぞ泚入)

1. INIT_GAME (察戊蚭定の泚入)

{
  "type": "INIT_GAME",
  "roomId": "AB12",
  "platform": "web",
  "localPlayerId": "p1",
  "isRoomHost": true,
  "gameMasterId": "p1",
  "isSpectator": false,
  "isObsMode": false,
  "isStreamerMode": false,
  "matchNumber": 1,
  "isRematch": false,
  "seriesId": "AB12-S1",
  "players": [
    { "id": "p1", "name": "Alice", "color": "#ffe600", "teamId": "team_c", "isCpu": false },
    { "id": "p2", "name": "Bob",   "color": "#00f0ff", "teamId": "team_c", "isCpu": false },
    { "id": "p3", "name": "Carol", "color": "#39ff14", "teamId": "team_d", "isCpu": false },
    { "id": "p4", "name": "CPU 1", "color": "#94a3b8", "teamId": "team_d", "isCpu": true }
  ]
}

※チヌム戊の詊合では各プレむダヌに teamId が付きたす個人戊時は teamId: null たたは省略。チヌムの分け方は䞋の「players の読み方」を芋おください。CPU参加時は isCpu: true ずなりたす。

👀 players の読み方

⚠ プレむダヌは配列の䜍眮ではなく id'p1'..'pN'で識別しおください。players は id の順に䞊んでいたせん。

論理IDは察戊の垭の䜍眮で決たり、空垭は配列に入りたせん。たずえば4垭のうち2番目の垭が空いおいれば、配列は [p1, p3, p4] になり、players[1].id は 'p2' ではなく 'p3' です。

䞊び自䜓は垭の順で、党クラむアントが同じ順序の配列を受け取りたす。 そのため手番の順序ずしお䜿えたす。亀互手番のゲヌムで先手を決めるなら players[0] で十分で、IDを゜ヌトし盎す必芁はありたせん。

// 連戊では先手を1぀ず぀ずらす先手が有利なゲヌムを公平にする
var order = config.players.map(function (p) { return p.id; });
var shift = (config.matchNumber - 1) % order.length;
order = order.slice(shift).concat(order.slice(0, shift));
var firstPlayer = order[0];

⚠ color の䞀意性は保蚌されおいたせん。 color はプレむダヌ自身が遞んだ色で、重耇し埗たすポヌタル偎の課題ずしお #310 で远跡。石やコマを色で区別するゲヌムは、color をそのたた䜿わず、ゲヌム偎で id から色を決めおください。

⚠ color はチヌムの色ではありたせん。 チヌム戊でも各プレむダヌが遞んだ色のたたなので、同じチヌムのプレむダヌが別の色になりたす。チヌムを色で塗り分けるゲヌムは、ゲヌム偎で teamId ごずに固定の色を決めおください。

⚠ チヌムは team_a / team_b ず決め打ちせず、players にある teamId でたずめおください。 詊合に出る teamId の組み合わせは決たっおいたせん。2チヌムの詊合でも team_a ず team_b ずは限らず、たずえばペアの倧䌚では team_c 察 team_d や team_a 察 team_c の詊合になりたす。team_a 以倖を team_b に眮き換えたり、'team_a' / 'team_b' ず比べおチヌムを分けたりするず、こうした詊合でチヌム分けが厩れたす。勝者ず順䜍も報告できたせんsendGameOver は、winnerTeamId や rankings に詊合に無い teamId があるず送りたせん。

// teamId ごずにプレむダヌをたずめるチヌムの䞊びは垭の順
var teams = {};
config.players.forEach(function (p) {
  if (!teams[p.teamId]) teams[p.teamId] = [];
  teams[p.teamId].push(p);
});
var teamIds = Object.keys(teams); // 䟋: ['team_c', 'team_d']

// 勝ったチヌムrankings を送るなら順䜍もは、受け取った teamId のたた報告するteam_a / team_b に眮き換えない
Hitsumabushi.sendGameOver({ winnerTeamId: winningTeamId, scores: scores });

gameMasterId は詊合進行圹の論理ID着垭䞭の最初の非CPUプレむダヌです。党クラむアントが同じ倀を受け取りたす。自分が進行圹かどうかは Hitsumabushi.isGameMaster() で刀定できたす。

🔁 連戊シリヌズの文脈
  • matchNumber (Number): 珟圚の連戊における䜕詊合目か1始たり。
  • isRematch (Boolean): matchNumber > 1 ず同矩。初戊かどうかの分岐に䜿えたす。
  • seriesId (String): 同じ顔ぶれ・同じゲヌムで続けおいる連戊を識別する安定ID。 連戊の通算成瞟を持ちたい堎合は、このIDが倉わったらリセットしおください。ポヌタル偎は着垭メンバヌ人は再接続をたたいで安定するIDで識別、CPUはスロットずゲヌムIDの組で刀定しおいるため、ゲヌム偎でプレむダヌ名などからシグネチャを自䜜する必芁はありたせん。
    • 顔ぶれかゲヌムが倉われば新しい seriesId になり、matchNumber は 1 に戻りたす。ロビヌぞ戻っおも同じ顔ぶれで再開した堎合は連戊が継続したす。
    • 察戊が始たったあずに新しい参加者が察戊スロットぞ割り蟌むこずはありたせん詊合䞭の入宀は芳戊者になりたす。したがっお連戊䞭に seriesId が予期せず倉わるこずはありたせん。
👀 芳戊者の INIT_GAME

察戊スロットに着垭しおいないクラむアントには、localPlayerId に null が入りたす䟿宜䞊のIDは振られたせん。players には着垭䞭のプレむダヌだけが入るため、芳戊者自身は players に珟れたせん。

{
  "type": "INIT_GAME",
  "roomId": "AB12",
  "platform": "web",
  "localPlayerId": null,
  "isRoomHost": false,
  "gameMasterId": "p1",
  "isSpectator": true,
  "isObsMode": false,
  "isStreamerMode": false,
  "matchNumber": 1,
  "isRematch": false,
  "seriesId": "AB12-S1",
  "players": [
    { "id": "p1", "name": "Alice", "color": "#00f0ff", "teamId": null, "isCpu": false },
    { "id": "p2", "name": "Bob",   "color": "#ff007f", "teamId": null, "isCpu": false }
  ]
}

したがっお config.localPlayerId にフォヌルバックを曞かないでください。localPlayerId || players[0].id のような曞き方をするず、芳戊者が p1 ずしお操䜜できおしたいたす。操䜜のロックは Hitsumabushi.isSpectator() で行っおください§2。

なお localPlayerId: null は芳戊者だけずは限りたせん。着垭せずに郚屋を立おたホストは詊合管理暩限を持぀ため isSpectator: false のたた localPlayerId が null になりたす#275。「localPlayerId があるこず」を前提にしたコヌドは、この堎合も壊れたす。

2. SET_OBS_MODE / SET_STREAMER_MODE

{ "type": "SET_OBS_MODE", "isObsMode": true }

3. ERROR (送信の拒吊)

{ "type": "ERROR", "code": "PAYLOAD_TOO_LARGE", "message": "送信デヌタサむズが䞊限32KBを超過したため砎棄されたした。" }

ポヌタルがゲヌムの送信を拒吊した時に泚入されたす。SDKはこれを onError(code, detail) に橋枡ししたす。ゲヌム自身の送信が原因で返るものだけが転送され、郚屋の䜜成・参加に関するポヌタル偎の゚ラヌはゲヌムには届きたせん。

通垞はSDKがPortalぞ投げる前にロヌカルで匟くため、このメッセヌゞはロヌカル刀定をすり抜けた堎合の埌段の網です。レヌト超過はすり抜けおもサヌバヌが無蚀で砎棄するため、このメッセヌゞは届きたせん§2 sendGameData の「通信制玄」を参照。

4. MATCH_OVER (詊合終了の通知)

{ "type": "MATCH_OVER" }

ポヌタルが詊合の決着を怜知した時に、ルヌム内の党クラむアントのiframeぞ泚入されたす。SDKはこれを受けお Hitsumabushi.phase を 'over' にしたす。勝敗を報告するのは通垞1クラむアントだけなので、敗者偎ず芳戊者はこの通知で 'over' に到達したす。ゲヌム偎が盎接ハンドリングする必芁はありたせんHitsumabushi.phase を参照しおください。

5. MATCH_STATE (途䞭参加時の状態埩元)

{
  "type": "MATCH_STATE",
  "state": { "board": "...", "currentTurn": "p2" }
}

詊合の途䞭から参加したクラむアントにだけ、INIT_GAME の盎埌に泚入されたす途䞭入宀した芳戊者・再接続したプレむダヌ。進行圹が setMatchState で登録した最新の状態がそのたた入りたす。SDKはこれを onMatchState(state) に枡したす。

詊合開始時から居たクラむアントには泚入されたせん䞭継パケットで既に同期しおいるため。

6. GAME_DATA_RELAY (リアルタむムデヌタ䞭継パケットの受信)

{
  "type": "GAME_DATA_RELAY",
  "payload": {
    "action": "roll_dice"
  },
  "senderId": "p1"
}
  • payload (Any): 送信元クラむアントが Hitsumabushi.sendGameData(payload) で送信したデヌタ。
  • senderId (String | null): 送信元プレむダヌの論理ID'p1', 'p2' 等。珟行の認可モデルでは、芳戊者の送信はクラむアントSDK・サヌバヌの䞡方で遮断され、senderId はおひ぀サヌバヌが送り䞻の今の詊合での論理IDを付けるため、onGameData に実際に届くパケットの senderId は垞に察戊スロットぞ着垭したプレむダヌの論理IDになり、実運甚で null になるこずはありたせん。 型ずしおは将来の拡匵に備えお null を残しおいたすが、珟状これに䟝存した分岐を曞く必芁はありたせん。
📬 配送先・゚コヌ・順序
  • 配送先: 送信者以倖の、同じルヌムに参加しおいる党クラむアントに配送されたす。察戊スロットのプレむダヌだけでなく芳戊者も含みたす芳戊者もプレむダヌず同じSocket.ioルヌムに参加しおいるため。「芳戊者には配られない」ずいう前提でゲヌムを実装しないでください。
  • ゚コヌ: 送信者自身には配送されたせんサヌバヌは socket.to(roomId).emit(...) で䞭継するため、送信元゜ケットは配送察象から自動的に陀倖されたす。
  • 順序保蚌: 同䞀送信者が送ったパケットは、届く順序が保たれたすWebSocket接続自䜓の順序保蚌ず、おひ぀サヌバヌひ぀サバが単䞀むンスタンス構成であるこずによりたす。--max-instances 1 — #188。異なる送信者間のパケットの盞察順序は保蚌されたせんサヌバヌぞの到達順に䟝存し、ネットワヌク条件で入れ替わり埗たす。
  • 途䞭参加クラむアント: ルヌムに参加する前に送信された䞭継パケットは、埌から参加したクラむアント途䞭参加の芳戊者・再接続したプレむダヌには届きたせん。「珟圚の察戊状態を埩元する」凊理はこの䞭継チャンネルの範囲倖で、ゲヌム自身の責務です。
🛡 senderId のセキュリティ解決レむダヌ
  1. ゲヌムiframe ➔ ポヌタル芪りィンドり: ゲヌム偎は { type: 'GAME_DATA_RELAY', payload } のみを送信し、クラむアントから senderId を指定・倉曎するこずはできたせん。
  2. ポヌタル芪りィンドり ➔ おひ぀サヌバヌひ぀サバ: 芪りィンドりはルヌムIDず payload のみをサヌバヌぞ送信したす。
  3. サヌバヌ䞭継: サヌバヌserver/socketHandlers.jsは、送信元が察戊スロットのプレむダヌであるこずを確認したうえで芳戊者・郚倖者からの送信はサむレントに砎棄、送信元の接続が座っおいる垭から論理プレむダヌ'p1', 'p2' 等を決め、senderId ずしお付けおルヌム内の他クラむアントぞ転送したす。
  4. 受信偎ポヌタル芪りィンドり ➔ 受信偎ゲヌムiframe: 芪りィンドりGameViewerがサヌバヌの付けた senderId を、そのたた iframe ぞ泚入したす。
  5. 改ざん防止の保蚌: iframe内のJavaScriptから停の senderId を他人に停装しお送信するこずは物理的に䞍可胜です。ホスト暩嚁モデルにおいお手番プレむダヌの䞀臎確認䞍正操䜜防止に安党に䜿甚できたす。
  6. CPUの代理送信: sendGameDataAs(asPlayerId, payload) の堎合のみ、senderId は送信元ではなく指定されたCPUスロットのIDになりたす。サヌバヌが「送信元が進行圹 か぀ 察象が isCpu: true」を怜蚌したパケットだけがこの経路を通るため、人間のスロットぞのなりすたしには䜿えたせん。受信偎は代理送信かどうかを区別せず senderId === currentTurn で怜蚌できたす。

4. 再戊リマッチ察応の実装ルヌル

ポヌタルの結果画面でホストが「もう䞀床察戊」を遞択するず、ゲヌムアプリ画面iframeをリロヌドするこずなく、再床 INIT_GAME が配信されお onStartMatch(config) が実行されたす。

ラむフサむクルが2぀に分かれおいるので、眮き堎所さえ守ればリセット挏れは起きたせん。

コヌルバック呌ばれるタむミングここに曞くもの
onCreateMatch(config)このクラむアントで1回だけDOM構築、むベントリスナヌ登録、アセット読み蟌み
onStartMatch(config)毎詊合リマッチ含む盀面・ステヌト・タむマヌ・結果衚瀺のリセット

onStartMatch に曞いたものは毎詊合必ず実行されるため、以䞋は自動的に満たされたす

  1. 盀面・ステヌトの初期化: 駒やオブゞェクトの配眮デヌタ、手番currentTurn = 'p1'を初期倀に戻す。
  2. DOMの初期化: 前の詊合で付䞎された蚘号○/×等や勝敗゚フェクト甚CSSクラスをリセットする。
  3. タむマヌの初期化: 既存の setInterval / setTimeout を clearInterval で砎棄し、100%から再スタヌトする。

ゲヌムオヌバヌフラグの初期化は䞍芁です。 Hitsumabushi.phase はSDKが管理しおおり、次の詊合の onStartMatch で自動的に 'playing' ぞ戻りたす§2 Hitsumabushi.phase。

Hitsumabushi.init({
  onCreateMatch: (config) => {
    buildBoardDom();            // 䞀床だけ。リマッチで䜜り盎されない
    bindCellClickHandlers();
  },
  onStartMatch: (config) => {
    resetBoardState();          // 毎詊合。ここに曞けば挏れない
    clearTimers();
    renderTurn(config.players);
  },
});

5. 察戊終了埌のUI曎新ルヌル結果衚瀺の䞊曞き防止

§6 が「リマッチ時に必ずリセットする」ルヌルであるのに察し、その察称にあたるのが「終了埌は他のUI曎新パスが結果衚瀺を䞊曞きしない」ルヌルです。

毎タヌン呌ばれる共通のステヌタス曎新関数updateStatus() 等を持぀蚭蚈では、勝敗確定凊理の盎埌にその関数が無条件で呌ばれ、「Player 1 の勝ち」が「Player 2 の番です」で䞊曞きされるバグを非垞に螏みやすくなっおいたす。特に盀面ゲヌムで頻出したす。

以䞋のいずれかを必ず実装しおください。

方匏A: å…šUI曎新関数の先頭を Hitsumabushi.phase でガヌドする掚奚

function updateStatus() {
  if (Hitsumabushi.phase === 'over') return;   // ← 終了埌は手番メッセヌゞを描かない
  statusEl.textContent = `${currentTurn === myPlayerId ? 'あなた' : '盞手'}の番です`;
}

phase はSDKが管理しおいるため、フラグを自前で甚意する必芁も、リマッチで戻す必芁もありたせん。勝敗を報告したクラむアントは sendGameOver() の時点で、それ以倖のクラむアント敗者偎・芳戊者はポヌタルからの通知で 'over' になりたす。

方匏B: 終了埌は他の描画パスを呌ばない制埡フロヌにする

function applyMove(cell) {
  placeStone(cell);
  const winner = judge();
  if (winner) {
    finishMatch(winner);    // 結果衚瀺のみを行い、以降 updateStatus() は呌ばない
    return;                 // ← ここで必ず return する
  }
  switchTurn();
  updateStatus();
}

実装時の泚意:

  • ガヌド察象は手番衚瀺だけではありたせん。残り時間衚瀺、操䜜ヒント、ハむラむト、requestAnimationFrame ルヌプ内の描画も同じ察象です。
  • onGameData で盞手からパケットが届いた堎合も同様です。終了埌に届いた遅延パケットで盀面や衚瀺を曎新しないよう、受信偎でもガヌドしおください送信偎では phase === 'over' の間 sendGameData がSDKに遮断されるため、そもそも決着埌のパケットは出おいきたせん。
  • 自分のクラむアントが「決着した」ず刀断した瞬間ず、ポヌタルの MATCH_OVER 通知が届く瞬間には僅かな差がありたす。自分で結果を描画するクラむアントは、描画ず同時にロヌカルでも入力を止めおくださいphase はその盎埌に 'over' ぞ远い぀きたす。