Python＋Streamlit でトランプゲームをコミュニティクラウドにデプロイする

### はじめに

<a href="https://www.python.org/"><img src="https://www.python.org/static/community_logos/python-logo.png" width="160"></a> <a href="https://streamlit.io/"><img src="https://streamlit.io/images/brand/streamlit-logo-primary-colormark-darktext.png" width="192"></a>

[Streamlit](https://streamlit.io/) は、Webアプリの手早い構築とサービス展開（deploy）を可能にするフレームワークです。

[Python](https://www.python.org/) ができることなら、たいていなんでも Web アプリ化できます。[Pandas](https://pandas.pydata.org/) と [Plotly](https://plotly.com/python/) を組みわせたデータビジュアライゼーションも、[OpenAI Whisper](https://openai.com/index/whisper/) を使った AI 文字起こしも、Streamlit でさくっと Web アプリに転換できます。お洒落なルーティング機構や HTTP/CSS は知らなくても、ほんの数行加えるだけでできあがりです。

自分の成果やアイデアを友人、共同研究者、同僚などと手軽に共有したいのなら、こんなに便利なものはありません。サーバ機能も付いてくるので、お手軽にローカル運用もできます。

感情分析の実装例は『[Python＋Streamlit で感情分析アプリをコミュニティクラウドにデプロイする](https://qiita.com/stoyosawa/items/fba8a836648b47d88b63)』で取り上げたので、そちらをご覧ください。記事では、自分のコードを Github 経由でクラウドにディプロイする方法も説明しています。


### イベント処理が苦手

とはいうものの、Streamlit はゲームのようにインタラクティブ性の高いアプリは得手ではありません。RPG キャラクタの移動程度であっても、ユーザ操作のイベントが発生すれば、スクリプトを先頭から再実行するからです。キャラ移動のたびにブラウザがリロードするのでは、あまり使い勝手がよいとはいえないでしょう。

状態管理には、リロードでも失わない専用の変数が使われます（Cookie のようなもの）。スクリプトは状態情報をもとに分岐することで、画面をその状態に応じてレンダリングします。そのため、状態依存性の高いアプリでは元のコードに Streamlit コマンドを加えるだけでは済まず、状態単位へのコードの分解と再構築が必要になってきます。

そのせいもあって、フリーなアプリ共有サイトの [Streamlit コミュニティクラウド](https://streamlit.io/community)でセレクトされている [Gallery](https://streamlit.io/gallery)（ギャラリー）にも、ゲームっぽいものはほとんどありません。そんな中でも、この「[ダンジョン](https://dungeon.streamlit.app/)」ゲームはかなり健闘しているので、お試しあれ。

<!-- 811 x 473 -->
<a href="https://dungeon.streamlit.app/"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712175/e3822410-2f84-4733-a76d-c371ab9ce135.png" width="400"></a>


### デモ

といいつつ、やってやれないわけではありません。ここではブラックジャックアプリを作り、インターネットに公開します。

ブラックジャックなのは、カードを取ったり切ったりする間合い（それぞれでイベントが発生する）が長めなので、ちょっとくらい反応が鈍くてもプレーできるからです。

本アプリは [`https://sat-blackjack.streamlit.app/`](https://sat-blackjack.streamlit.app/) からアクセスできます。スリープしていたら、［Yes, get this app back up!］とある青いボタンから再稼働してください。

次に画面例を示します。

<!-- 1454 x 808 -->
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712175/9638e1e1-17a3-4189-bc9a-4c594d97e088.png" width="700">

左パネル上の［Start］ボタンをクリックすれば、カードが2枚配られ、ゲームが開始します。カードを取るには［Hit］、勝負に出るには［Showdown］です。シンプルなゲームなので遊び方は自明だと思いますが、詳しい説明は右パネル上の［遊び方］プルダウンを見ください。


### アプリのコード

コードは長い（空行やコメントも入れて170行くらい）ので、[Github](https://github.com/stoyosawa/streamlit/) を参照してください。

外部ライブラリは使っていないので、`requirements.txt`は必要ありません。これだけで動きます。

コードの説明はざくっと割愛しますが、トランプをどのように表現しているかと、ボタンのアクティブ・インアクティブを状態に応じて変化させる方法だけを説明します。


### トランプの表現

Streamlit も Python も Unicode 対応なので、カードの絵柄は Unicode 文字で表示できます。コードポイントは `U+1F0A0` から `U+1F0F5` までの間です（Wikipedia の “[Playing cards in Unicode](https://en.wikipedia.org/wiki/Playing_cards_in_Unicode)” を参照）。わたしたちが普通に使うトランプがその順番通りに並んでいれば話が早いのですが、ナイト（JQK ではなく C!）なんていう絵札があったりして、柄（♠♡♦♧）と数値（1～13）からコードポイントを計算するのがちょいとややこしいので、本スクリプトでは直書きで52枚を並べて使っています。

```python
cards_surface = list('🂡🂢🂣🂤🂥🂦🂧🂨🂩🂪🂫🂭🂮'+'🂱🂲🂳🂴🂵🂶🂷🂸🂹🂺🂻🂽🂾'+
    '🃁🃂🃃🃄🃅🃆🃇🃈🃉🃊🃋🃍🃎'+'🃑🃒🃓🃔🃕🃖🃗🃘🃙🃚🃛🃝🃞')
```

ブラウザ上のフォントの標準サイズは16ポイント程度なので、そのまま表示したのでは絵柄の区別など付きません。そこで、CSSスタイルで強引に96ポイントまで上げて表示します。ついでに、♡と♦は赤、♠と♧は濃い青で色付けます。

```python
cards_style = [f'<span style="font-size: 96px; color: {c};">'
    for c in ['MidnightBlue', 'Red', 'Red', 'MidnightBlue']]
```

Streamlit の文字列表示は Github スタイルマークダウンが基本ですが、マークダウンでは無視される CSS を併用するには `st.html()` でレンダリングさせます。


### ボタンのアクティブ切り替え

リロードでも消えない状態管理の変数は `st.session_state` です。これは辞書で、任意のキーと値を収容できます。本スクリプトでは、ボタンのアクティブ・インアクティブをキー `state_buttons` に収容しています。値は辞書で（なので辞書の辞書）、キー `Start` はスタートボタン、`Hit` は1枚めくりボタン、`Showdown` は勝負のボタンです。初期状態では、次のようになっています。

```python
def set_buttons(start=False, hit=True, showdown=True):
    st.session_state.states_buttons['Start'] = start
    st.session_state.states_buttons['Hit'] = hit
    st.session_state.states_buttons['Showdown'] = showdown
```

値が `True` ならインアクティブなので、デフォルトでは `Start`（`False`）だけがアクティブで、他はインアクティブです。

スタートボタンが押されたら、次の要領で、それ自体をインアクティブに、他をアクティブにします。

```python
    set_buttons(True, False, False)
```

ボタン表示の Streamlit コマンドは `st.button()` です。この関数には `disabled` キーワード引数があり、`True` を指定すれば、インアクティブになります。

```python
    st.button('Start', disabled=st.session_state.states_buttons['Start'],
      on_click=clicked_start)
    st.button('Hit', disabled=st.session_state.states_buttons['Hit'],
      on_click=clicked_hit)
    st.button('Showdown', disabled=st.session_state.states_buttons['Showdown'],
      on_click=clicked_showdown)
```

キーワード引数の `on_click` はおなじみコールバック関数登録ですが、JavaScript と違って、その関数だけが呼び出されるわけではありません。スクリプトはやはり、最初から最後まで実行されます。ただし、登録関数はスクリプトのどのラインよりも先に動作することが保証されています。

スタートボタンがクリックされれば、リロードされ、最初に `clicked_start()` 関数が実行されます。この関数では、上記のアクティブ・インアクティブの状態を変化させ、山札を準備し、ディーラーとプレーヤーに手札を配り、それぞれを `st.session_state` に収容します。また、最下端のエリアに示すメッセージを用意します。別のボタンがクリックされればその関数が実行され、手札を変更し、スコアを計算し、メッセージを変えます。

あとはそれぞれの描画エリアに文字列を表示させるだけです。書籍の表紙のある左パネルなどはほとんど中身が変わりませんが、ボタンクリックのたびに再描画されています。


### おわりに

と、駆け足で Streamlit の魅力を紹介しました。コードはほとんど説明しませんでしたが、状態管理をつうじて、ゲームのような複雑な動きも達成できることがどことなくわかってもらえれば幸いです。

本書の記事は、技術評論社の許可を得て[『作ってわかる［入門］Streamlit 〜 Pythonによる実践Webサービス開発』](https://gihyo.jp/book/2025/978-4-297-14764-8)（2024年2月刊）から理解・分解・再構築して作成したものです。

ご購入はこちらから：【[Honto](https://honto.jp/ebook/pd_34122316.html)，[amazon.co.jp](https://www.amazon.co.jp/dp/4297147645)，[ヨドバシ](https://www.yodobashi.com/product/100000086602699652/)，[楽天ブックス](https://books.rakuten.co.jp/rb/18113489/)】

<img src="http://image.gihyo.co.jp/assets/images/cover/2025/9784297147648.jpg" width="200">