Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

ついたて将棋 サンプル bot on AWS Lambda

ついたて将棋ビューワーの bot を AWS CloudFront + WAF + Lambda として実装する最小テンプレートです。

Lambda はサイトの dispatcher から手番リクエストを受け取り、HMAC 署名を検証してから、bot の指し手を move として返します。

準備

Python 3.12 のほか、AWS CDK、AWS CLI、Node.js/npm が必要です。
AWSアカウントの認証設定、cdk bootstrap などを事前に行う必要があります(参考)。
この README に記載しているコマンドは、主に macOS / Linux 向けです。Windows の方は適宜読み替えてください。

このサンプルでは、ついたて将棋のエンジンとしてこちらのライブラリを使用しています。Lambda Linux/arm64 向けの wheel は上記レポジトリの scripts/build_py_game_lambda_wheel.sh により作成できます。作成された target フォルダをこちらのルートフォルダ直下に配置してください。

Lambda に deploy する wheel と、ローカルでテストを実行する wheel は別物です。

  • deploy 用: Lambda と同じ Linux/arm64 / Python 3.12 向け wheel が必要です。macOS / Windows / x86_64 Linux で作った通常の wheel は Lambda arm64 では動きません。
  • ローカルテスト用: 自分の PC の OS / CPU / Python に合う wheel をインストールします。tests/unit/test_strategy.pytsuitate_bindings を import するため、未インストールだと失敗します。

ローカルで tsuitate_bindings を使う場合は、tsuitate-shogi-crates/tsuitate_bindings/ フォルダで以下を実行してください。

pip install maturin
python -m maturin develop

デプロイ方法

ルートフォルダ直下 target/wheels に Lambda Linux/arm64 向けの tsuitate_bindings wheel を配置してから deploy します。

配置例:

tsuitate_sample_bot/
  target/
    wheels/
      tsuitate_bindings-...-linux_aarch64.whl
  app/
    app.py
    cdk.json
cd app
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

WebhookPath は Webhook Secret とは別のランダムな値にしてください。macOS / Linux では以下のように生成できます。

openssl rand -hex 16

WebhookSecret は必須パラメータです。ついたて将棋ビューワーへの登録前はまだ発行されていないため、初回 deploy では仮の値を使います。このとき、あとで使い続ける WebhookPath も同時に指定してください。

cdk deploy \
  --parameters WebhookSecret=temporary-secret-before-registration \
  --parameters WebhookPath=/webhook/<上で生成したランダム値>

deploy 後に出力される BotEndpointUrl を bot の endpoint として、ついたて将棋ビューワーに登録します。

登録時に表示される Webhook Secret を使用して、同じ WebhookPath のまま再度 deploy します。Webhook Secret はあとから再表示できないので、安全な場所に控えてください。

cdk deploy \
  --parameters WebhookSecret=<ついたて将棋ビューワーで返されたwebhookSecret> \
  --parameters WebhookPath=/webhook/<初回deployと同じランダム値>

以下は必須ではないですが、セキュリティを高めるための推奨手順です。

deploy 後、AWS のコンソール上 CloudFront から BotCloudFrontDomainName の distribution を Free plan (CloudFront Free flat-rate plan) に紐づけます。

Free plan 紐づけ後、plan 側で関連付けられた AWS WAF WebACL に以下のルールを設定してください。

Name Action 条件
RateLimitByIp Block Rate-based rule。Aggregate は Source IP、5 分間あたり 1000 requests。

AWSリソースを一通り削除するには、以下のコマンドを実行します。

cdk destroy

Free plan への紐づけを行った場合は、エラーになる場合があります。
そのときは、AWS コンソールの CloudFormation のページからスタックの削除をお願いします。

テスト

CDK template のテストのみ実行する場合:

cd app
pip install pytest
pytest tests/unit/test_app_stack.py

bot の指し手生成も含めてテストする場合は、ローカル環境に合う tsuitate_bindings wheel を先にインストールしてから実行してください。

cd app
pip install pytest
pytest

パラメータ

  • WEBHOOK_SECRET: ついたて将棋ビューワーで一度だけ返されるシークレット。CDK の WebhookSecret パラメータから設定されます。
  • MAX_BODY_BYTES: 受け付けるリクエストの最大バイト数。未指定時は 128 * 1024
  • WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS: HMAC timestamp の許容秒数。未指定時は 300
  • GAME_STATE_TABLE_NAME: 対局ごとの game と結合済み positions を保存する DynamoDB テーブル。CDK により自動設定されます。
  • GAME_STATE_TTL_SECONDS: 対局状態の有効期間。CDK では 86400(1 日)に固定されています。対局状態を更新するたびに期限も延長されます。
  • ReservedConcurrency: Lambda の同時実行数上限。未指定時は 2。非常時は 0 に変更すると停止できます。
  • WebhookPath: bot 登録に使う webhook path。HMAC secret とは別のランダム値を指定してください。

dispatcher からのリクエストモデル

登録した Webhook endpoint には、bot の手番になるたびに JSON 形式の POST リクエストが届きます。初回リクエストには手数 0 から現在までの全局面と game が含まれ、2 回目以降は前回から増えた局面だけが送られます。最新の仕様はついたて将棋ビューワーの Bot 開発者向け仕様も参照してください。

Headers:

Content-Type: application/json
X-Tsuitate-Bot-Id: {botId}
X-Tsuitate-Timestamp: {unixSeconds}
X-Tsuitate-Signature: sha256={hexHmac}
x-amz-content-sha256: {hexSha256OfBody}

Body:

type Position = {
  sfen: string;
  fouls?: { b: number; w: number };
  times?: { b: number; w: number };
  byoyomiActive?: { b: boolean; w: boolean };
  lastMove?: string;
  lastInfo?: number;
  lastCapture?: string;
  wasPromotion?: boolean;
};

type Game = {
  type: string;
  param: string;
  requiredPlayers: { b: number; w: number };
};

type BotTurnRequestBase = {
  requestId: string;
  gameId: string;
  color: "b" | "w";
  number: number;
  ply: number;
};

type InitialBotTurnRequest = BotTurnRequestBase & {
  positions: Record<string, Position>; // 手数 0 から現在まで
  game: Game;
};

type SubsequentBotTurnRequest = BotTurnRequestBase & {
  basePly: number;
  positions: Record<string, Position>; // basePly + 1 から現在まで
  game?: never;
};

type BotTurnRequest = InitialBotTurnRequest | SubsequentBotTurnRequest;

パラメータの意味:

  • requestId: リクエストごとの識別子。
  • gameId: 対局の識別子。局面履歴と game はこの値ごとに保持する。
  • color: bot の手番。b は先手、w は後手。
  • number: 同じ色の中で何番目の参加者かを表す 0 始まりの番号。リレー形式の場合のみ 1 以上になり得る。
  • ply: 初期盤面を 0 とした手数。反則手も手数に含める。一方で sfen の末尾に含まれる番号は、反則手を除く着手数 + 1 (初期盤面 1)である。
  • basePly: 2 回目以降のリクエストにのみ含まれる、bot が保持済みと想定される最終手数。
  • positions: 手数の文字列をキーにした局面履歴。初回は手数 0 から現在まで、2 回目以降は basePly + 1 から現在までが含まれる。
  • sfen: そのレコード時点で bot から見えている局面
  • fouls: 残り反則数
  • times: 残り時間
  • byoyomiActive: 秒読みに入ったか
  • lastMove: 直前手。CSA 形式。
    • 例: +7776FU
    • 相手の lastMove はマスクされたものが送られる。基本は +0000ZZ / -0000ZZ で、lastCapture がある場合のみ +0076ZZ のように移動先のマスが開示される。
  • lastCapture: 直前手で獲得した駒。CSA 形式。例: FU
  • lastInfo: 直前手の情報。王手・反則判定に使う。値の意味は以下の通り。
INFO_NONE = 0
INFO_FOUL = 1
INFO_FOUL_UNDER_CHECK = 2
INFO_CHECK = 3
INFO_CHECKMATE = 4
  • wasPromotion: 直前手で成ったかどうか。相手の指し手は常に false にマスクされる。
  • game.type: ダーク | ついたて | ついたて5五 | ついたてリレー | カスタム のいずれか
  • game.param: ゲームのパラメータ。game.typeカスタム のときはここから初期盤面などの設定を読み取る必要がある。パラメータの記法は、ついたて将棋ビューワーを参照のこと。
  • game.requiredPlayers: プレイヤー数。リレー形式の場合のみ 2 以上になり得る。

初回リクエスト

その対局で bot に届く最初のリクエストには、手数 0 から現在までの全局面と game が含まれます。

{
  "requestId": "4f31a682d9c04b55b51aa231:0:b:0",
  "gameId": "4f31a682d9c04b55b51aa231",
  "color": "b",
  "number": 0,
  "ply": 0,
  "positions": {
    "0": {
      "sfen": "9/9/9/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
      "fouls": { "b": 9, "w": 9 },
      "times": { "b": 300, "w": 300 },
      "byoyomiActive": { "b": false, "w": false }
    }
  },
  "game": {
    "type": "ついたて",
    "param": "&promotion_rank=3&enable_try_rule=false&n_players=1.1&visibility_rule=tsuitate&time_limits=180.180&fischer_increment=0&byoyomi=30&foul_limits=9.9&draw_move_count=150",
    "requiredPlayers": { "b": 1, "w": 1 }
  }
}

2 回目以降のリクエスト

basePly は bot が保持済みと想定される最終手数です。game は含まれず、positions にはその次の局面から現在局面までが入ります。

{
  "requestId": "4f31a682d9c04b55b51aa231:2:b:0",
  "gameId": "4f31a682d9c04b55b51aa231",
  "color": "b",
  "number": 0,
  "ply": 2,
  "basePly": 0,
  "positions": {
    "1": {
      "sfen": "9/9/9/9/9/9/PP1PPPPPP/1B5R1/LNSGKGSNL w - 2",
      "fouls": { "b": 9, "w": 9 },
      "times": { "b": 294, "w": 300 },
      "byoyomiActive": { "b": false, "w": false },
      "lastMove": "+7776FU",
      "lastInfo": 0
    },
    "2": {
      "sfen": "9/9/9/9/9/9/PP1PPPPPP/1B5R1/LNSGKGSNL b - 3",
      "fouls": { "b": 9, "w": 9 },
      "times": { "b": 294, "w": 298 },
      "byoyomiActive": { "b": false, "w": false },
      "lastMove": "-0000ZZ",
      "lastInfo": 0
    }
  }
}

bot は gameId ごとに gamepositions を保持してください。2 回目以降は、保持済みの最終手数が basePly と一致することを確認してから、受信した positions を同じ手数のキーで追加します。上の例では、保持している手数 0 に手数 1 と 2 を追加します。

このサンプルでは DynamoDB に対局状態を保存します。保存単位は bot、対局、手番側、プレイヤー番号の組み合わせです。状態は最後の更新から 1 日で期限切れになり、DynamoDB TTL により自動削除されます。TTL の削除は非同期であるため、期限を過ぎた項目が物理的に残っている場合でも handler は利用しません。

検証とレスポンス

  • HMAC署名を検証する。
  • timestampが5分以上ずれているリクエストを拒否する。
  • 初回は gamepositions を保存し、2 回目以降は basePly を検証して局面履歴を更新する。
  • 現在局面を読み、指し手を決定する。
  • 指し手を move として返す。

bot のレスポンス例

{
  "move": "+7776FU"
}

HMAC検証手順

  1. raw request bodyを取得する。
  2. X-Tsuitate-Timestamp を取得する。
  3. 現在のtimestampと WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS 以上ずれている場合は拒否する。
  4. timestamp + "." + rawBody に対してHMAC-SHA256を計算する。
  5. X-Tsuitate-Signature と定数時間比較する。
  6. 検証に失敗したらHTTP 401または403を返す。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages