npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

def-game

v4.0.0

Published

- a framework for creating online board games

Readme

def-game

  • a framework for creating online board games

このフレームワークは、オンラインボードゲームを作成する際の開発を支援する。 フレームワークは、開発者が定義するべき情報を明確に分類する。

このフレームワークを使って、ゲームルールを定義した共通モジュールを作成する。 その共通モジュールはサーバーサイドプロジェクトとクライアントサイドプロジェクトからimportすることを前提とする。 開発者は、サーバーサイド側のコードを修正する必要がない。 共通モジュールで定義したゲームロジックがそのまま動くようになっているからである。 また、サーバークライアント間での型共有が容易になるのも利点の一つである。

ここからは、このフレームワークが要求するデータ型や関数についての説明を行う。 固有の用語データ型については、インラインコードブロックで示す。

設計 | design

アウトライン

このフレームワークが要求する関数のすべては、GameRuleに定義されている。

export interface GameRule<T extends Map, S extends Map, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> {
    initialGameLogicState: (staticValues: any) => L;
    createRoom: (state: GameState<T, S, C, L, P>, param: GameParameters) => GameState<T, S, C, L, P>;
    onStartGame: (state: GameState<T, S, C, L, P>, joiner: Player) => GameState<T, S, C, L, P>;
    generateTasks(state: GameState<T, S, C, L, P>, event: ToServer<C>): Task<C, T>[];
    prioritizeTasks(newTasks: Task<C, T>[], gameState: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
    doTasks(state: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
}

initialGameLogicState

initialGameLogicState: (staticValues: any) => L; ゲームロジック状態のデフォルトを作成する関数である。 これはプレイヤーが参加する前に仮に作成されるゲーム状態である。 引数のstaticValuesは、静的な設定値のオブジェクトである。これはプログラム上にハードコードされたものである。

ここで重要なのは、できる限り関数(計算)の中でstaticValuesから他の値を算出することである。 これは、ランダムマップの生成や、ゲーム内の計算で使用される値などを算出することを念頭においている。 逆にinitialGameLogicStateの中でのハードコードは避け、staticValuesにすべてを集約することも大切である。

このようにしてゲームロジック状態のデフォルトLが作成される。これはルームが作成される前の段階である。

createRoom

createRoom: (state: GameState<T, S, C, L, P>, param: GameParameters) => GameState<T, S, C, L, P>; この関数は、ルームが作成されるタイミングで実行される関数である。 開発者は、GameParametersを設定することができる。 これはルームを作成する段階で、入力可能な設定値のことである。画面から入力されることを想定している。

例えば、プレイヤーの人数や制限時間、ゲームに固有なパラメータなどなんでもゲームロジック状態に設定できる。 そして、この関数は、GameStateが持つGameParametersを更新することを行う。 これは、プレイヤーの待ち合わせ処理などのゲームロジック状態とは分離して処理を行う場合を想定しており、GameParametersはそこで使用される。

それに加えて、必要であれば、ゲームロジック状態に含まれる設定値などもこのタイミングで更新を行う。 それらを全ての更新を行なったGameState<T, S, C, L, P>を返す。

この段階で、ルームに参加しているプレイヤーに対しては、UIを提供する必要があり、 UIに関する情報は、ゲームロジック状態の中に保持することを推奨している。

onStartGame

onStartGame: (state: GameState<T, S, C, L, P>, joiner: Player) => GameState<T, S, C, L, P>; これは、プレイヤーの参加ごとに呼び出される関数である。 プレイヤーの待ち合わせ処理を実装する。 参加したプレイヤーに対しては適切なUI情報を返す。 この関数が実行されている段階ですでにWebSocket接続が確立されている。

generateTasks | prioritizeTasks | doTasks

この3つの関数は、ゲームの進行を定義するのに重要な関数となる。 サーバーサイドプロジェクトにデフォルトで実装されている処理フローは、src/simulatir.tsで確認することができる。 以下の部分である。

        // ------ Game Logic ------
        const tasks = this.rules.generateTasks(state, action);
        const updatedQueueState = this.rules.prioritizeTasks(tasks, state);
        const newState = this.rules.doTasks(updatedQueueState);

タスクというゲーム状態を更新する最小の単位となる概念を導入している。 ゲーム状態タスクキューをもつ。

  1. generateTasks(state: GameState<T, S, C, L, P>, event: ToServer<C>): Task<C, T>[];

プレイヤーからのイベントを受け取った時に、実行されるのがこの関数である。 この関数ではゲーム状態を更新しない。新しく発生するタスクの配列を返す。

この関数では、イベントを受け取った際のバリデーションを実装することができる。

  1. prioritizeTasks(newTasks: Task<C, T>[], gameState: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;

この関数では、ゲーム状態が持つタスクキューgenerateTasksにより生成された新しいタスクの配列をマージする処理を実装する。 タスクの優先度に応じて並び替える処理を実装できる。

  1. doTasks(state: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;

この関数は、タスクキューをループして、一つ一つのタスクを実行する処理を記述する。 ここで重要なのは、一つのタスクがタスクリザルトを返すことである。

タスクリザルトは、次のタスクを実行するのか、一旦停止して処理を終えるのかと言った情報を持つことができる。 この処理は、特にユーザー間のやり取りが発生する場合に有用である。

  • ユーザーA: request
  • タスク生成: (task1, task2, task3, task4)
  • task1はユーザーBからの応答を要請する
    • タスクの実行を一時中断: (task2, task3, task4)
  • ユーザーB: request
  • タスク生成: (task1B)
    • 優先度による並び替え: (task1B, task2, task3, task4)
  • タスクキューの実行再開

また、より堅牢なシステムを作るために、次に期待されるタスクをゲーム状態に持たせることが可能である。 この例でいえば、"task1はユーザーBからの応答を要請する"際に、タスクリザルトは、タスクの実行を一時中断するだけでなく、次に期待されるタスクを"task1B"に設定する。 これにより、再開時に、"task1B"以外の実行をキャンセルする。

privaten

privaten(destinationId: PlayerId, l: L): L

privaten(destinationId, gameLogicState): GameLogicStateは情報秘匿のための関数である。 基本的には、ゲームロジック状態が更新されるたびに、ゲームロジック状態をシリアライズしたものをクライアント側に通知する。 クライアントからゲームロジック状態の一部を隠さなければならない状況に対処するため、この関数を実装する。

これはサーバーサイドに実装されているbroadcast関数であり、各プレイヤーごとにprivatenを呼び出す。

async broadcast<T>(result: Game1State) {
    Object.keys(result.gameLogicState.forClient).forEach(async to => {
        await this.sendTo(to, JSON.stringify(gameRule.privaten(to, result.gameLogicState)));
    });
}

型による支援

規則的な命名と強い型付によって、型システムやAIによる補完の精度向上を狙っている。

GameRule<T extends Map, S extends Map, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> を見てもらえれば、分かる通りいくつかのRecord型(Map)定義を要請している。

T extends Map

これは、タスクを定義するためのMapである。

interface Tasks {
    "sample": SampleTask
    "update": UpdateTask
}

と言った具体である。 タスクハンドラーは、ゲームロジック状態とこのタスク(例えばSampleTask型のデータ)を受け取り、ゲームロジック状態を更新する処理を行う。

S extends ResMap

これは、サーバーサイドから返却されるUI状態を定義するためのMapである。

interface Res {
    "updated": Notify<UpdatedRes>
    "accept": Require<AcceptRes>
}

ResMapは、値の型として、NotifyRequireのいずれかでラップすることを要請する。 これは、UI上で、ただの情報の通知を表すか、ユーザーに対してのなんらかのアクションの要求を表している。 Requireは、アクションに関する制限時間を持っており、ユーザーに伝えることが可能である。

C extends ReqMap

これは、クライアントサイドからイベントは発生させるためのリクエストを定義するためのMapである。

interface Req {
    "propose": Custom<ProposeReq>
    "accept": Simple<AcceptReq>
}

ReqMapは、値の型として、CustomSimpleのいずれかでラップすることを要請する。 これはサーバーサイド側での実装を簡略化するために導入された型である。

なんらかのReqを受け取ってからTask[]に変換する処理はしばしば冗長である。 そこでリクエストペイロードをそのままタスクとして定義することができるようになっている。 この例では、Simple<AcceptReq>のようにSimpleによってラップされたペイロード(AcceptReq)は、そのままタスクとしてキューに追加される。

この仕組みは、以下の型定義により、型的にもサポートされている。

export type TaskMapType<A extends ReqMap, B extends Map> = Merge<SimpleOnlyPayload<A>, CustomTaskMap<B>>;
export type Task<A extends ReqMap, B extends Map> = {
    [K in keyof TaskMapType<A, B>]: {
        type: K;
        payload: TaskMapType<A, B>[K];
    }
}[keyof TaskMapType<A, B>];

その他の設計

ルームへの接続

  • プラットフォームAPIにより認証されたユーザーは、ルームIDを指定して入室することができる。
  • このタイミングでroomKeyと呼ばれるJWTを返す。 { playerId: playerId, roomId: body.roomId }
  • クライアントは、そのJWTをWebSocket接続時のパラメータに加える。
  • これにより、接続とplayerIdが関連づけられる。
  • WebSocket接続を通じて送られる全てのJSONデータはplayerIdプロパティを持っている。
  • サーバーサイドは、そのplayerIdプロパティがWebSocket接続に紐づいたplayerIdと一致することを確認する。

時間制限

ルームに接続中のクライアントは、webSocketハートビートを定期的に送信する。 これは、要求されたアクションの時間制限に関する確認処理をトリガーする。

gameState.nextTaskExpectationは以下のような情報を持っていて、

{
    expectedTaskType: "accept",
    deadline: 1744589266,
    defaultTask: {
        playerId: "samplePlayer",
        proposerId: "proposer",
        isAccept: false,
    }
}

deadlineを過ぎていれば、defaultTaskタスクキューの先頭に配置され、実行が再開するという具体である。