DebtCraft には、ゲーム内のお金(/sell やデイリーボーナスで貯まる)と、借金を多く返した人がもらえる「功労ポイント」がある。その使い道として、借金時計のサイトに国営ショップを作った。
売っているもの
- 称号:チャットの名前の後ろに
[国債王]のように付く - 特権:土地保護のブロック追加、
/hat、/craft、/ec - くじ:1回100億円の国営宝くじと、功労ポイントで引くガチャ
宝くじは、平均すると払った額より少し損をするように当たりの確率を決めた。
設計:Webは「注文を入れるだけ」
一番気をつけたのは、Webからお金を動かさないこと。
- ブラウザで商品を選ぶ
- Web(Node.js)が PostgreSQL の
shop_ordersにstatus='pending'の行を入れる - マイクラのプラグインが数秒ごとに注文を取り出し、お金を引いて商品を渡し、結果を書き戻す
- ブラウザは結果が出るまで注文を見に行く(くじはその間スロットが回る)
値段はプラグインの config にだけ書いてあり、Web からは商品のIDしか送らない。ページを改ざんして値段を書き換えても意味がない。
DB の権限も絞った。Web 用のユーザーは、注文の表に3つの列しか INSERT できない。
GRANT INSERT (uuid, discord_id, product_id) ON shop_orders TO debtcraft_shop;
status を直接 done にした注文を入れることも、お金やポイントを書き換えることもできない。
プラグイン側は、注文を FOR UPDATE SKIP LOCKED で1件ずつ取り出して、二重に処理しないようにしている(プラグインは Claude Code に指示書を渡して作ってもらった)。
ログインはDiscordで
マイクラのアカウントでWebにログインさせるのは面倒だが、DebtCraft はもともと「Discordのボットでマイクラの名前を登録する」ホワイトリスト制にしている。DB に Discord ID とマイクラの名前の対応がすでにあるので、Discord の OAuth2(scope は identify だけ)でログインすれば、誰がどのプレイヤーかが分かる。
ログイン状態は、HMAC で署名したクッキーに入れている。サーバー側にセッションを保存しないので、コンテナを作り直してもログインが切れない。
見つかった穴:OPは残高マイナスまで払えた
テスト中に、所持金の表示が「—」のまま買い物ができてしまった。調べると、ショップ自体は正しく残高を確かめていて、表示がまだ届いていなかっただけだった。
ただ、調べる途中で別の穴が見つかった。EssentialsX には essentials.eco.loan(借金してでも払える)という権限があり、OP は持っている。Vault の has() はこの権限があると常に true を返すので、所持金ゼロでも /hensai(所持金からの返済)が通ってしまう状態だった。
お金を引く処理を1か所にまとめ、getBalance() で「残高 − 金額 < 0 なら引かない」と確かめるように直した。一般のプレイヤーはこの権限を持っていないが、借金を返すゲームで「借金して返済できる」のは笑えない。
財務省チャンネルの自動投稿
Discord の「財務省」カテゴリーのチャンネルにも、ボットが自動で投稿するようにした。
| チャンネル | 投稿 |
|---|---|
| 📉 借金時計 | 毎日0時に、今の借金・1日で増えた額・国民が返した額・前日のMVP |
| 🏆 長者番付 | 毎日0時に、累計の返済額トップ10 |
| 🚨 滞納者リスト | 毎週月曜の納税の締めのあと |
| 📰 ニュース速報 | ダイヤ64個分以上の返済、借金が100兆円の区切りをまたいだとき、ランクアップやくじの大当たり |
ボットは DB を読むだけ。どこまで投稿したかをファイルに保存しているので、再起動しても同じ速報を二度出さない。人がいない時間でもチャンネルが動くので、サーバーが生きているように見える。
ちなみに、借金は1日に約30兆円増える設定になっている。現実の日本の1年分が、毎日増える。