前の記事で取引所とオークションを作った。今回は「カジノがほしい」「ダイヤをブロックのまま納めたい」という声に応えた。あわせて、本番に入れたあとに見つかった細かい問題も直した。
国営カジノ
遊べるのは次の3つ。
- スロット:3つそろえば最大300倍。チェリーが2つでも2倍
- ハイロー:次のカードが今より高いか低いかを当てる。当たるほど倍率が上がり、好きなところで降りられる(最大10連続)
- ブラックジャック:ディーラーは17で止まる。ブラックジャックは2.5倍、ダブルダウンあり
ゲーム内では /casino の画面で、Web ではカジノのページで遊べる。どちらで始めても同じ勝負として記録される。
還元率は100%より少し下に
「借金を返すサーバー」なので、カジノで国庫が減っては困る。そこで、遊ぶほど少しずつ国庫に入るように倍率を決めた。
- スロット:出やすさの重みと配当から計算して、還元率は約95%
- ハイロー:「当たる確率の逆数 × 0.95」を倍率にしたので、どこで降りても約95%
- ブラックジャック:普通のルールのままで約98%
スロットの還元率は、重みと配当から計算で出せる。手で決めた数字のまま出さず、表にしてから合わせ込んだ。
Web は乱数に触らない
国営ショップ・取引所と同じ作りにした。
- ブラウザで「回す」「ヒット」などを押す
- Web は
casino_requestsという表に「依頼」の行を入れるだけ - プラグインが依頼を取り出し、カードを配ったりリールを回したりして、お金を動かし、結果を書き戻す
- ブラウザは結果を見に行って表示する
カードを引くのも、お金を動かすのもプラグインだけ。Web が乱数を持つと、ブラウザの開発ツールで結果をいじられる心配が出てくるからだ。
ブラックジャックで気をつけたのは、ディーラーの伏せたカード。勝負の状態は Web が読める表に入れるので、伏せたカードまで入れると、DB を読める人にはディーラーの手が見えてしまう。そこで、伏せたカードだけは別の表に分けて、Web 用の読み取り専用ユーザーからは読めないように権限を外した。
鉱石ブロックで納める・売る
ダイヤを9個ずつ持ち歩くのは面倒なので、ブロックのまま /nouzei と /sell に出せるようにした。
ここで大事なのは、ブロックを別の品目にしないこと。このサーバーは、同じ品目をたくさん納めると相場が下がる。ブロックを別の品目にすると、ダイヤとダイヤブロックを交互に納めて、値下がりを避けられてしまう。
そこで、「ダイヤブロック1個 = ダイヤ9個」と換算してから、相場・1日の基準量・クエストの進み具合まで、すべてダイヤとして扱った。記録もダイヤ18個として残すので、Web のランキングなどは何も変えずに正しく出る。
名前を付けたら差し押さえを逃れられる?
実装の途中で、Claude Code が「名前やエンチャントが付いたアイテムは、納める・売るの対象外にした。差し押さえの対象からも外れる」と報告してきた。
前の半分はよいが、後ろの半分は困る。金床でダイヤに名前を付けるだけで、差し押さえを逃れられてしまうからだ。差し押さえだけは名前やエンチャントを無視して取るように直してもらった。「受け付けない」と「取り上げない」は、同じ判定を使い回すと抜け道になる。
朝の「おはよう」でお金がもらえる
Discord の雑談チャンネルで、朝5時から11時までに「おはよう」と書くと、1日1回お金がもらえるようにした。お金はボットが RCON でゲーム内に渡す。
Discord のボットがメッセージの中身を読むには、開発者ポータルで MESSAGE CONTENT INTENT を ON にする必要がある。ON にしないまま起動すると、PrivilegedIntentsRequired で起動に失敗する。
スマホのメニューを見直した
Web の下のメニューに8個のボタンが並んでいて、「押しにくい」「押し間違えそう」と言われた。1個あたりの幅が45pxほどしかなく、文字も折り返していた。
- よく使う「国庫・番付・取引所・カジノ」と「メニュー」の5個に減らした
- 残りの「滞納者・遊び方・参加する・ショップ」は、「メニュー」を押すと下から出る一覧に、説明付きの大きなボタンで並べた
- このメニューを1つのファイル(
nav.js)にまとめ、取引所・カジノ・ショップのページでも同じものを出すようにした
スロットの画面も、スマホの縦画面では右にはみ出していた。原因は CSS Grid の列の幅で、列の幅を決めていないと、中身の一番広い幅まで列が広がってしまう。grid-template-columns: minmax(0, 1fr) を指定して、画面幅に収めた。
本番に入れるときにハマったこと
バックアップが書けない
su - postgres -c "pg_dump ... > /root/..." は、postgres ユーザーとして /root に書こうとするので権限エラーになる。> を su の外に出して、root としてファイルに書くようにした。ついでに、DB のコンテナではなくホスト側に保存されるので、バックアップとしてもこの方がよい。
scp で「Received message too long」
root で送ろうとしたら、Received message too long 1349281121 と出た。この数字を16進数にすると 0x506C6561 で、文字にすると「Plea」になる。サーバーが root のログインを断って「Please login as ...」と返し、その文字を scp が通信のデータとして読んでしまっていた。送るユーザーを変えたら通った。
新しい機能が入っていない
ボットを入れ替えたのに、おはようの機能が動かない。ビルドのログを見ると、casino.py がコピーされていなかった。デスクトップに置いていた zip が、機能を足す前の古いものだった。前回の「入れ替えたのに変わらない」と同じで、置いたファイルが本当に新しいかを先に見るべきだった。
Web の所持金が変わらない
「カジノで遊んでも、Web の所持金が変わらない。ショップでは変わるのに」と言われた。
Web が見せている所持金は、プラグインが DB に書いた写しで、書き直すのはログイン・ログアウトしたとき、ゲーム中の1分ごと、ショップで買った直後だけだった。カジノと取引所には「処理した直後に書き直す」決まりがなかったので、ゲームに入っていない人は、Web で遊んでも写しが古いままだった。
カジノ・取引所・オークションのどれでも、お金が動いた直後にその人の写しを書き直すように直した。Web 側も「最大1分おくれ」という正しくない表示をやめ、「○分前の残高」と出すようにした。
学んだこと
- 乱数やお金を扱う処理は、ゲームと Web のどちらから使っても、1か所(プラグイン)にまとめる
- 秘密にしたい値は、同じ表に入れて Web 側で隠すのではなく、別の表に分けて権限で守る
- 「受け付けない」と「取り上げない」のように意味の違う判定は、使い回さない
- スマホの下メニューは5個までにして、残りはまとめて出す