乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

プログラミング

ルーレットなどのギャンブル
元手が倍になる確率はいくらになるでしょうか?


解説動画はこちら




問題

カジノのルーレットで元手が倍になる確率はどれくらいでしょうか?

ルール

100ドルを元手に 1ドル 単位で賭け続け
200ドル に到達するか0になったら終了

数字は1-36まであり、赤か黒の色が設定されている
数字0(緑)が1個、ここに当たるとディーラーが勝ち、賭けは没収

赤か黒に賭けると、当たったら2倍の払い戻し

このルールだと、元手が2倍になる確率はどれくらいでしょうか?




回答

ルーレットの元手が倍になる確率は
ギャンブラーの破産問題の公式に当てはめると解けるようです。

スタート資金を i、目標を N、負けたら破産(0)
勝率 p、負け率 q=1-p のとき
目標に到達する確率は以下で表せます:
スクリーンショット 2025-09-20 17.00.23

この式に代入していくと

•    i = 100 
•   N = 200 
•   p = 18/37  = 0.48648649
•   q = 19/37  = 0.51351351

こんな感じになり
計算式に当てはめると

スクリーンショット 2025-09-20 17.01.19
となります。

最終的な確率を求める部分を
Pythonのコードでやってみましょう。
p =  (1 - (19/18) ** 100) / ( 1 - (19/18) ** 200 )
print(p)
print(f"{p*100:.04}%")
0.004466284539492003
0.4466%


大体、0.45%くらい
1000回挑戦したら、4-5回でしょうか。




ルーレットのシミュレーションコード

これを実際にシミュレーションしてみます。

コードを簡略化するために
赤と黒の代わりに奇数なら当たり
偶数ならハズレにしてやってみます。
import random

def simulate_game(start=100, goal=200, trials=10):
    bankrupt_count, double_count = 0, 0
    for _ in range(trials):
        money = start
        while 0 < money < goal:
            # ルーレットの出目を 0-36 からランダムに選ぶ
            spin = random.randint(0, 36)
            if spin % 2 == 0:  # 偶数 → 負け
                money -= 1
            else:  # 奇数 → 勝ち
                money += 1
        if money == 0:
            bankrupt_count += 1
        elif money == goal:
            double_count += 1

    print(f"シミュレーション回数: {trials}")
    print(f"破産した回数: {bankrupt_count}")
    print(f"200ドルに到達した回数: {double_count}")
    print(f"破産確率: {bankrupt_count/1000:.3f}")
    print(f"成功確率: {double_count/1000:.3f}")

simulate_game(trials=1000)
シミュレーション回数: 1000
破産した回数: 997
200ドルに到達した回数: 3
破産確率: 0.997
成功確率: 0.003


おおよそ、数式の数値を近くはなりますね。

1000人いたら
4-5人は元手を増やしてカジノを出ることができる
そんな確率ですかね。



ルーレット以外の破産確率

1.ブラックジャック(基本戦略でプレイ)
2.パチンコ(一般的な遊技機)
3.競馬(単勝を繰り返す単純モデル)

この破産確率も見てみます。

ここでは計算を簡略化するために
1ゲームあたりの勝率を使って近似確率を求めていきます。

1ゲームあたりの勝率は以下のように設定することとします。
•   ブラックジャック(基本戦略+カジノ側有利)
 → プレイヤー勝率 ≈ 0.49、負け ≈ 0.51

•   パチンコ(通常 0.80〜0.90 程度の還元率)
 → 実質勝率 ≈ 0.45、負け ≈ 0.55

•   競馬(通常 0.70〜0.80 程度の還元率)
 → 実質勝率 ≈ 0.40、負け ≈ 0.50 と仮定


ルーレットにこの3つも加えてシミュレーションするコード
from decimal import Decimal, getcontext
getcontext().prec = 50  

def ruin_probability(s, N, p):
    """
    ギャンブラーの破産確率
    s: 初期資金
    N: 目標資金
    p: 勝率
    """
    q = Decimal(1) - p
    if p == q:
        return Decimal(1) - Decimal(s) / Decimal(N)
    else:
        ratio = q / p
        num = ratio**Decimal(s) - ratio**Decimal(N)
        den = Decimal(1) - ratio**Decimal(N)
        return num / den

# パラメータ
s = Decimal(100)   # 初期資金
N = Decimal(200)   # 目標資金

games = {
    "ブラックジャック": Decimal("0.49"),
    "ルーレット" : Decimal("0.48648649"),
    "パチンコ": Decimal("0.45"),
    "競馬": Decimal("0.40")
}

for game, p in games.items():
    ruin = ruin_probability(s, N, p)
    success = Decimal(1) - ruin
    print(f"{game}:")
    print(f"  破産確率  = {ruin:.20}")
    print(f"  倍化確率  = {success:.20}")
    print()
ブラックジャック:
  破産確率  = 0.98202320998693248326
  倍化確率  = 0.017976790013067516735

ルーレット:
  破産確率  = 0.99553370920702988362
  倍化確率  = 0.0044662907929701163797

パチンコ:
  破産確率  = 0.99999999807255307809
  倍化確率  = 1.9274469219075612712E-9

競馬:
  破産確率  = 0.99999999999999999754
  倍化確率  = 2.4596544265798292632E-18


ルーレットは先ほどと同じなので他を見ると
ブラックジャックは意外と勝てそうですね

競馬とパチンコはもう
勝てる見込みがほぼ無いです。


競馬だと
単勝を同じ金額ずっと買い続けるみたいなものが
この確率に近くなります。

単純な買い方では
競馬で元手を増やすことは出来ないでしょうね。



まとめ

1回あたりのおよその勝率から
破産確率は導き出すことができます。

基本的にギャンブルは元手を倍にできる確率は非常に少ないので
(ブラックジャックだけは、勝てる見込みが0ではない)
同じ100万円を注ぎ込むなら、まだ投資に回したほうがいいでしょう。

ギャンブルは卒業しよう!!!

ギャンブルでお金を減らす人が
一人でもいなくなりますように

それでは。


今回は15年間の積立投資シミュレーションです。

SP500とオルカンを毎月10万円15年間
積み立てたらどうなるかをシミュレーションします。


解説動画はこちら




S&P500 VS オールカントリー

今回は積立投資のシミュレーションです。

15年間積立投資を続けていたら、いくらになるのか
レバレッジかけた場合も合わせて検証してみます。


Yファイナンスの株価データを元に
シミュレーションしていきます。

Yファイナンスでの取得コードは
オルカン(ACWI)
S$P500(^GSPC)
となっているので、簡単にデータを取得できます。

シミュレーション条件は
2010年9月から2025年9月までの
15年間分(180ヶ月)のデータで
毎月10万円(新NISAの1800万円分)

という条件で行きたいと思います。

レバレッジは2倍と3倍のも
追加して検証していきます。



Yファイナンスの株価データを取得する


Google ColabではYファイナンスのライブラリで
簡単に株価を取得できるようになっています。

早速データを取得していきましょう。
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

# オルカン(All Country World Index)
acwi_ticker = "ACWI"

# S&P 500
sp500_ticker = "^GSPC"

# 過去15年間の株価データを取得
acwi = yf.download(acwi_ticker, start="2010-09-01", end="2025-09-05")
sp500 = yf.download(sp500_ticker, start="2010-09-01", end="2025-09-05")
acwi_close = acwi["Close"]
sp500_close = sp500["Close"]
acwi_monthly = acwi_close.resample("MS").first()
sp500_monthly = sp500_close.resample("MS").first()
取得用の株価コードは
オルカン : ACWI
S$P500 : ^GSPC
となっていて

株価コード、開始日、終了日を
指定するだけで簡単にデータ取得できます。



株価推移をプロット


この15年間の両者の株価をプロットしてみます。
# オルカン
plt.figure(figsize=(14, 3))
plt.plot(acwi_close, label="ACWI")
plt.title("ACWI - Past 15 Years")
plt.ylabel("Closing Price")
plt.legend()
plt.grid(True)
plt.show()
# S&P500
plt.figure(figsize=(14, 3))
plt.plot(sp500_close, label="S&P 500")
plt.title("S&P 500 - Past 15 Years")
plt.ylabel("Closing Price")
plt.legend()
plt.grid(True)
plt.show()

download-2
download-3

株価が違いすぎるので、一緒にプロットできないので
正規化して、同じプロット内で表示してみましょう。
# 正規化
acwi_norm = acwi_close / acwi_close.iloc[0]
sp500_norm = sp500_close / sp500_close.iloc[0]

# グラフの作成
plt.figure(figsize=(14, 5))
plt.plot(acwi_norm, label="ACWI")
plt.plot(sp500_norm, label="S&P 500")
plt.title("ACWI vs S&P 500 - Past 15 Years")
plt.ylabel("Norm Price")
plt.legend()
plt.grid(True)
plt.show()
download-1

この15年で株価は何倍になっています。

このデータを用いてシミュレーションしていきます。


積み立てシミュレーション


これが今回のシミュレーション用の関数です。
# --- シミュレーション関数 ---
def simulate_investment(prices, monthly_invest=100000, leverage=1.0):
    returns = prices.pct_change().dropna()
    shares = 0.0
    portfolio = []

    for i, r in enumerate(returns):
        # 毎月積立で購入
        shares += monthly_invest / prices.iloc[i]
        shares *= (1 + r * leverage)  # レバレッジ調整後の値動き
        portfolio.append(shares * prices.iloc[i+1])  # 翌月末時点の評価額

    portfolio = pd.Series(portfolio, index=returns.index)
    return portfolio

実際にシミュレーションするコードはこちら
毎月の投資額を入力すると計算、可視化できます。
# データの結合
df = pd.concat([acwi_monthly,sp500_monthly],axis=1)
# 毎月の投資額
monthly_invest = 100000

# 直近のみに絞るならコメントアウト外す
#df = df.tail(60)

df_assets = pd.DataFrame({
    "S&P500_1x": simulate_investment(df["^GSPC"], monthly_invest, leverage=1.0),
    "S&P500_2x": simulate_investment(df["^GSPC"], monthly_invest, leverage=2.0),
    "S&P500_3x": simulate_investment(df["^GSPC"], monthly_invest, leverage=3.0),
    "ACWI_1x": simulate_investment(df["ACWI"], monthly_invest, leverage=1.0),
    "ACWI_2x": simulate_investment(df["ACWI"], monthly_invest, leverage=2.0),
    "ACWI_3x": simulate_investment(df["ACWI"], monthly_invest, leverage=3.0)
})
df_assets["Input_Amount"] = [(i+1) * monthly_invest for i in range(len(df_assets))]
# プロット
plt.figure(figsize=(14, 5))
plt.plot(df_assets.index, df_assets["S&P500_1x"], label="S&P500 (1x)", color="orange")
plt.plot(df_assets.index, df_assets["ACWI_1x"], label="ACWI (1x)", color="blue")
plt.plot(df_assets.index, df_assets["S&P500_2x"], label="S&P500 (2x)", color="orange", linestyle="-.")
plt.plot(df_assets.index, df_assets["ACWI_2x"], label="ACWI (2x)", color="blue", linestyle="-.")
plt.plot(df_assets.index, df_assets["S&P500_3x"], label="S&P500 (3x)", color="orange", linestyle="--")
plt.plot(df_assets.index, df_assets["ACWI_3x"], label="ACWI (3x)", color="blue", linestyle="--")
plt.plot(df_assets.index, df_assets["Input_Amount"], label="Input Amount", color="black")
plt.title("S&P500 vs ACWI leverage simulation")
plt.ylabel("Asset amount")
plt.grid(True)
plt.legend()
ax = plt.gca()
ax.yaxis.set_major_formatter(ticker.ScalarFormatter(useOffset=False))
ax.ticklabel_format(style="plain", axis="y")
plt.show()
download-4

直近5年に絞り込むには
df のコメントアウトの部分を外して
直近分に絞り込みするとできます。

download

どれもこれも、ずっと持ち続けていたら
資産はすごいことになっていたでしょうね。

レバレッジをかけた商品の場合
上下動が激しすぎて
途中で手放してしまった
なんてこともありそうです。

ずっと持ち続けていたら
投資額を上回っていた、ということにはなります。

この株価のデータは今後も同じ動きをするわけではないので
再現性は0ですが、積立投資自体は
悪い選択肢ではないかなと思っています。

今後両者がどのような動きをするかは
気になるところです。

今回は
SP500とオルカンの
積立投資シミュレーションでした。

それでは。




今回は老後の資産をどうやって取り崩すか
資産取り崩しのシミュレーションです


解説動画はこちら




老後資産取り崩し


老後の資産を年利 X % で運用しつつ
毎月 X 万円取り崩していく際の
資産推移をシミュレーションします。


資産金額、年利、リスク確率から
破産確率がどれくらいになるのか
これを導き出します。



モンテカルロシミュレーション


資産取り崩しの際のシミュレーションに使うのが
モンテカルロシミュレーションです。

これはサイコロをたくさん振って
結果を求めるようなイメージです。

おおよその内容としては


資産がスタート(例:5,000万円)
毎月、資産が「投資リターン」で少し増えたり減ったりする

例:平均 4%/年、リスク(ブレ幅) 1%/年
これを「正規分布」というサイコロで乱数を作って計算
その後「毎月の生活費を取り崩す」。(例:20万円ずつ減らす)
これを1シナリオとして 最後まで計算

これを 1,000回くらい繰り返す
サイコロを振って、資産の未来を1000通り描くイメージです。

コードでは1000回の結果を可視化していきます。



ライブラリのインストール



Google Colab で使用するには
日本語のライブラリが必要なのでこれを入れておいてください

# 可視化ライブラリのインストール
pip install japanize-matplotlib


モンテカルロシミュレーションのコード

ipywidgetsとmatplotlibで可視化するコードです。

コピペして上から実行していくと
スライダーとボタンが出るはずです。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
import ipywidgets as widgets
from IPython.display import display, clear_output

# シミュレーション関数
def simulate_withdrawal(start_age, years, initial_assets, monthly_withdraw, annual_return, annual_risk, n_sim=1000):
    months = years * 12
    results = []
    for _ in range(n_sim):
        assets = initial_assets
        monthly_returns = np.random.normal(
            loc=(annual_return/100)/12,
            scale=(annual_risk/100)/np.sqrt(12),
            size=months
        )
        trajectory = []
        for r in monthly_returns:
            assets *= (1 + r)           
            assets -= monthly_withdraw  
            assets = max(0, assets)     
            trajectory.append(assets)
        results.append(trajectory)
    return pd.DataFrame(results).T  


# 実行関数
def run_simulation(b):
    clear_output(wait=True)
    display(ui)
    
    # 入力値取得
    start_age = age_slider.value
    years = years_slider.value
    initial_assets = assets_slider.value
    monthly_withdraw = withdraw_slider.value
    annual_return = return_slider.value
    annual_risk = risk_slider.value

    # シミュレーション実行
    df = simulate_withdrawal(start_age, years, initial_assets, monthly_withdraw, annual_return, annual_risk)
    mean_path = df.mean(axis=1)
    median_path = df.median(axis=1)
    best_path = df.max(axis=1)
    worst_path = df.min(axis=1)
    months = np.arange(1, years*12 + 1)

    # 最終結果テキスト
    final_assets = df.iloc[-1]
    print(
        f"最終結果:"
        f"  破産確率: {100 * (final_assets==0).mean():.1f}%"
        f"  平均資産額: {final_assets.mean():,.0f} 万円"
        f"  中央資産額: {final_assets.median():,.0f} 万円"
        f"  最良ケース: {final_assets.max():,.0f} 万円"
        f"  最悪ケース: {final_assets.min():,.0f} 万円"
    )

    # グラフ描画 (matplotlib)
    plt.figure(figsize=(10,6))
    #plt.plot(months, best_path, label="最良ケース", color="green")
    plt.plot(months, worst_path, label="最悪ケース", color="red")
    plt.plot(months, mean_path, label="平均ケース", color="blue")
    plt.plot(months, median_path, label="中央値ケース", color="orange")
    plt.title(f"資産取り崩しシミュレーション(開始年齢: {start_age}歳, 期間: {years}年)")
    plt.xlabel("経過月数")
    plt.ylabel("資産額 (万円)")
    plt.legend()
    plt.grid(True)
    plt.show()


# スライダーUI
age_slider = widgets.IntSlider(value=65, min=50, max=100, step=1, description="開始年齢")
years_slider = widgets.IntSlider(value=30, min=10, max=50, step=1, description="年数")
assets_slider = widgets.IntSlider(value=5000, min=1000, max=20000, step=100, description="資産(万円)")
withdraw_slider = widgets.IntSlider(value=20, min=10, max=100, step=1, description="月取崩(万円)")
return_slider = widgets.IntSlider(value=4, min=1, max=20, step=1, description="利回り%")
risk_slider = widgets.IntSlider(value=1, min=1, max=30, step=1, description="リスク%")

# 実行ボタン
run_button = widgets.Button(description="シミュレーション実行", button_style="success")
run_button.on_click(run_simulation)

# UIまとめて表示
ui = widgets.VBox([age_slider, years_slider, assets_slider,withdraw_slider, return_slider, risk_slider,run_button])
display(ui)
スクリーンショット 2025-08-30 17.39.21

スライダーを調整して
シミュレーションボタンクリックで
結果が出ます。

スクリーンショット 2025-08-30 17.12.13


老後の資産と毎月の取り崩しの金額
利回りとリスクを変更すると

どれくらいの確率で資産が破産するか
求めることができます。

破産確率が0%になるように
取り崩しをしていくと資産が残る形になります。

毎月どれくらい取り崩せるのか
おおよその金額を算出できるので
老後資金が足りるかどうか
資産したい場合は、ぜひ使ってみてください。

老後2000万円問題とか
言われていますが
2000万円では全然足りないですねーーーーーー


資産が底を尽きないように
どんどん増やしていけるように
したいと思っています。

今後も投資系のシミュレーションを
どんどんやっていきますので
リクエストがあれば、どんどん
動画のコメントに書き込んでみてください

それでは。

今回はオンカジに有りそうな
ポーカーBOTについてです。


解説動画はこちら


 

この内容はあくまでも
BOTプログラムに関する内容となります。




プログラムの内容


ポーカーのテキサスホールデムルールのゲームを行える
Streamlitプログラムです。

BOT用のストラテジークラスがあり
それを改変することでBOTの戦略を
構築することができるようになっています。




テキサスホールデムのルール

自分だけの手札2枚と場に共有される5枚のカード
(コミュニティカード)を組み合わせて
最も強い5枚の役を作るポーカーゲーム

ゲームはブラインドベットで開始され
プリフロップ、フロップ、ターン、リバーの
4つのラウンドでそれぞれベットが行われる

各ラウンドでは
コール、レイズ、フォールド、チェックなどのアクションを選択し
最終的に残ったプレイヤーの中で
最も強い役を持っていたプレイヤーがポットを獲得



今回のBOTの特徴

プリフロップはそれなり
ポストフロップは運任せ
という戦略です。

細かいルールは

ハンド強度評価を行い
3段階の強度分類でアクションを決めます。

評価基準(スコア10-50程度)

ペア: +30 + ハイカードランク
AA = 30 + 14 = 44点(最強)
KK = 30 + 13 = 43点
22 = 30 + 2 = 32点

スーテッド: +5点
同じスーツなら追加ボーナス

ハイカード: high/2 + low/5
AK = 14/2 + 13/5 = 7 + 2.6 = 9.6点
スーテッドAK = 9.6 + 5 = 14.6点


強度分類

強いハンド (45点以上)
主にAA, KK, QQ相当
積極的にレイズ/ベット
チップが少なければオールイン

中程度のハンド (35-44点)
中位ペア、AK、強いスーテッドコネクター
チェック/コール中心
守備的プレイ

弱いハンド (34点以下)
低ペア、アンスーテッドローカード
チェックできればチェック
ベットがあればフォールド


こんな戦略をとるBOTです。


プログラムの詳細

3つのプログラムからなっています。
main.py : StreamlitのUIなど
poker_engine.py : ポーカーゲームの制御
bot_strategy.py : BOTのストラテジー

ローカルでStreamlitを動かす場合はインストールが必要です。
pip install streamlit


main.py
import streamlit as st
from poker_engine import PokerGame
from bot_strategy import BotStrategy

def init_game():
    """ゲーム初期化"""
    player_names = ["You"] + [f"BOT{i+1}" for i in range(7)]
    st.session_state.game = PokerGame(player_names)
    st.session_state.game.new_hand()
    st.session_state.pending_action = None

def process_pending():
    """保留中のアクションを処理"""
    if st.session_state.pending_action is not None:
        idx, action = st.session_state.pending_action
        st.session_state.pending_action = None
        st.session_state.game.apply_action(idx, action)

def auto_advance_bots_until_user_or_showdown(max_steps=50):
    """Botの連続ターンを自動進行"""
    game = st.session_state.game
    steps = 0
    
    while steps < max_steps:
        if game.phase == "showdown":
            return
        
        current_player = game.get_current_player()
        if current_player is None:
            return
        
        if current_player.name == "You" or not current_player.can_act():
            return
        
        # Bot行動
        bot = BotStrategy(current_player.name)
        action = bot.decide_action(
            current_player.hand, 
            game.community, 
            current_player.chips,
            game.current_bet, 
            current_player.bet_this_round
        )
        
        game.apply_action(game.current_index, action)
        steps += 1

def draw_player_panel():
    """プレイヤー情報パネルを描画"""
    game = st.session_state.game
    cols = st.columns(8)
    
    for i, player in enumerate(game.players):
        with cols[i]:
            # プレイヤー名の装飾
            name = player.name
            if player.allin:
                name += " (ALL-IN)"
            if i == game.dealer_index:
                name += " [D]"
            if i == game.sb_index:
                name += " [SB]"
            if i == game.bb_index:
                name += " [BB]"
            
            # 非アクティブ・フォールドプレイヤーは取り消し線
            if not player.active or player.folded:
                st.markdown(f"~~**{name}**~~")
            else:
                st.markdown(f"**{name}**")
            
            # チップ表示
            st.write(f"Chips: {player.chips}")
            
            # ベット額表示
            if player.bet_this_round > 0:
                st.write(f"Bet: {player.bet_this_round}")
            
            # あなたの手札のみ表示
            if player.name == "You" and player.hand:
                st.write(f"Hand: {player.hand[0]}  {player.hand[1]}")

def draw_community_area():
    """コミュニティエリアを描画"""
    game = st.session_state.game
    
    st.subheader("Dealer Zone")
    
    # コミュニティカード表示
    if game.community:
        community_str = " ".join(game.community)
        st.write(f"Community: {community_str}")
    else:
        st.write("Community: (No cards yet)")
    
    # ポット表示
    st.write(f"Pot: {game.pot}")
    
    # ゲーム状況メッセージ
    st.info(game.message)

def draw_action_interface():
    """アクションインターフェースを描画"""
    game = st.session_state.game
    
    if game.phase == "showdown":
        st.success("Hand complete. Click 'New Hand' to continue.")
        if game.game_over:
            st.warning("Game over. Your chips are gone or opponents busted. Click 'New Hand' to restart.")
        return
    
    current_player = game.get_current_player()
    if current_player is None or current_player.name != "You":
        # あなたのターンではない
        if current_player:
            st.info(f"Waiting for {current_player.name} to act...")
        return
    
    if not current_player.can_act():
        return
    
    # アクション選択UI
    st.subheader("Your Turn")
    
    legal_actions = game.legal_actions(game.current_index)
    to_call = max(0, game.current_bet - current_player.bet_this_round)
    
    # アクションラベルを整形
    action_labels = []
    for action in legal_actions:
        if action == "call":
            action_labels.append(f"Call ({to_call})")
        elif action == "bet":
            action_labels.append("Bet (10)")
        elif action == "raise":
            action_labels.append("Raise (+10)")
        else:
            action_labels.append(action.capitalize())
    
    # ラジオボタンでアクション選択
    selected_action = st.radio(
        "Choose your action:",
        legal_actions,
        format_func=lambda x: action_labels[legal_actions.index(x)],
        horizontal=True,
        key="player_action"
    )
    
    # アクション実行ボタン
    if st.button("Submit Action", type="primary"):
        st.session_state.pending_action = (game.current_index, selected_action)
        st.rerun()

def main():
    st.set_page_config(
        page_title="Texas Hold'em Poker",
        page_icon="♠",
        layout="wide"
    )
    
    st.title("♠ Texas Hold'em Poker")
    st.markdown("---")
    
    # ゲーム初期化
    if "game" not in st.session_state:
        init_game()
    
    # 上部コントロール
    col1, col2, col3 = st.columns([1, 1, 2])
    
    with col1:
        if st.button("New Hand", type="secondary"):
            st.session_state.game.new_hand()
            st.rerun()
    
    with col2:
        if st.button("Restart Game", type="secondary"):
            init_game()
            st.rerun()
    
    with col3:
        game = st.session_state.game
        st.write(f"Phase: **{game.phase.title()}** | Current Bet: **{game.current_bet}**")
    
    st.markdown("---")
    
    # 保留中のアクションを処理
    process_pending()
    
    # Botの自動進行
    auto_advance_bots_until_user_or_showdown()
    
    # プレイヤー情報表示
    st.subheader("Players")
    draw_player_panel()
    
    st.markdown("---")
    
    # コミュニティエリア表示
    draw_community_area()
    
    st.markdown("---")
    
    # アクションインターフェース
    draw_action_interface()

if __name__ == "__main__":
    main()






poker_engine.py
import random
import itertools
from typing import List, Dict, Tuple, Optional

# ========= 基本定義 =========
SUITS = ["♠", "♥", "♦", "♣"]
RANKS = ["2","3","4","5","6","7","8","9","T","J","Q","K","A"]
RANK_VAL = {r:i for i,r in enumerate(RANKS, start=2)}
INITIAL_CHIPS = 1000

def create_deck() -> List[str]:
    """52枚のデッキを作成"""
    return [r+s for r,s in itertools.product(RANKS, SUITS)]

# ========= ハンド評価 =========
def hand_rank(cards5: List[str]) -> Tuple:
    """
    5枚のカードから役の強さを評価
    返り値: (rank_class, high cards...) で大きい方が強い
    rank_class: 8=ストレートフラッシュ, 7=フォーカード, 6=フルハウス,
               5=フラッシュ, 4=ストレート, 3=スリーカード, 2=ツーペア, 1=ワンペア, 0=ハイカード
    """
    vals = sorted([RANK_VAL[c[0]] for c in cards5], reverse=True)
    suits = [c[1] for c in cards5]
    counts = {}
    for v in vals:
        counts[v] = counts.get(v, 0) + 1
    by_count = sorted(counts.items(), key=lambda x:(x[1], x[0]), reverse=True)
    is_flush = len(set(suits)) == 1
    
    # ストレート判定(Aを1として扱うホイール対応)
    uniq = sorted(set(vals), reverse=True)
    def straight_high(vs):
        run = 1
        best = None
        for i in range(len(vs)-1):
            if vs[i] - 1 == vs[i+1]:
                run += 1
                if run >= 5:
                    best = vs[i-3]
            elif vs[i] != vs[i+1]:
                run = 1
        # A-5ストレート
        if set([14,5,4,3,2]).issubset(set(vals)):
            best = max(best or 0, 5)
        return best
    
    straight_hi = straight_high(uniq)

    if is_flush and straight_hi:
        return (8, straight_hi)
    if by_count[0][1] == 4:
        four = by_count[0][0]
        kicker = max([v for v in vals if v != four])
        return (7, four, kicker)
    if by_count[0][1] == 3 and any(c==2 for _,c in by_count[1:]):
        triple = by_count[0][0]
        pair = max([v for v,c in by_count[1:] if c==2])
        return (6, triple, pair)
    if is_flush:
        return (5, *vals)
    if straight_hi:
        return (4, straight_hi)
    if by_count[0][1] == 3:
        triple = by_count[0][0]
        kickers = [v for v in vals if v != triple][:2]
        return (3, triple, *kickers)
    pairs = [v for v,c in by_count if c==2]
    if len(pairs) >= 2:
        top, sec = sorted(pairs, reverse=True)[:2]
        kicker = max([v for v in vals if v not in [top,sec]])
        return (2, top, sec, kicker)
    if len(pairs) == 1:
        pair = pairs[0]
        kickers = [v for v in vals if v != pair][:3]
        return (1, pair, *kickers)
    return (0, *vals)

def best_hand_7(cards7: List[str]) -> Tuple[Tuple, List[str]]:
    """7枚から最強の5枚役を評価"""
    best = None
    best5 = None
    for comb in itertools.combinations(cards7, 5):
        h = hand_rank(comb)
        if (best is None) or (h > best):
            best = h
            best5 = list(comb)
    return best, best5

# ========= プレイヤークラス =========
class Player:
    def __init__(self, name: str, chips: int = INITIAL_CHIPS):
        self.name = name
        self.chips = chips
        self.hand = []
        self.folded = False
        self.allin = False
        self.active = True
        self.bet_this_round = 0
        self.has_acted = False
    
    def reset_for_new_hand(self, hand: List[str]):
        """新しいハンド用にリセット"""
        if self.chips <= 0:
            self.active = False
            self.folded = True
            self.allin = False
            self.hand = []
            self.bet_this_round = 0
            self.has_acted = True
        else:
            self.folded = False
            self.allin = False
            self.hand = hand
            self.bet_this_round = 0
            self.has_acted = False
    
    def reset_for_new_round(self):
        """新しいラウンド用にリセット"""
        self.bet_this_round = 0
        self.has_acted = False
    
    def can_act(self) -> bool:
        """行動可能かどうか"""
        return self.active and not self.folded and not self.allin
    
    def post_blind(self, amount: int) -> int:
        """ブラインドを支払う"""
        actual = min(self.chips, amount)
        self.chips -= actual
        self.bet_this_round += actual
        if self.chips == 0:
            self.allin = True
        return actual

# ========= ゲームエンジンクラス =========
class PokerGame:
    def __init__(self, player_names: List[str]):
        self.players = [Player(name) for name in player_names]
        self.dealer_index = 0
        self.sb_index = 0
        self.bb_index = 0
        self.deck = []
        self.community = []
        self.pot = 0
        self.phase = "preflop"  # preflop, flop, turn, river, showdown
        self.current_bet = 0
        self.current_index = 0
        self.last_raiser = None
        self.message = ""
        self.game_over = False
    
    def new_hand(self):
        """新しいハンドを開始"""
        self.deck = create_deck()
        random.shuffle(self.deck)
        self.community = []
        self.pot = 0
        self.phase = "preflop"
        self.current_bet = 0
        self.last_raiser = None
        
        # ディーラーボタン順回し
        self.dealer_index = (self.dealer_index + 1) % len(self.players)
        self.sb_index = (self.dealer_index + 1) % len(self.players)
        self.bb_index = (self.dealer_index + 2) % len(self.players)
        
        SB_amount = 10
        BB_amount = 20
        
        # 配牌と状態リセット
        for player in self.players:
            if player.chips <= 0:
                player.reset_for_new_hand([])
            else:
                hand = [self.deck.pop(), self.deck.pop()]
                player.reset_for_new_hand(hand)
        
        # ブラインドを支払う
        sb_posted = self.players[self.sb_index].post_blind(SB_amount)
        bb_posted = self.players[self.bb_index].post_blind(BB_amount)
        self.pot += sb_posted + bb_posted
        self.current_bet = BB_amount
        
        # プリフロップの最初の行動者はBBの次
        self.current_index = self._next_index(self.bb_index)
        self.message = f"New hand: Dealer={self.players[self.dealer_index].name}, SB={self.players[self.sb_index].name}({sb_posted}), BB={self.players[self.bb_index].name}({bb_posted})"
    
    def _alive_players(self) -> List[int]:
        """生存プレイヤーのインデックスリスト"""
        return [i for i, p in enumerate(self.players) if p.active and not p.folded]
    
    def _next_index(self, idx: int) -> Optional[int]:
        """次の行動可能プレイヤーのインデックス"""
        n = len(self.players)
        for step in range(1, n+1):
            j = (idx + step) % n
            if self.players[j].can_act():
                return j
        return None
    
    def _everyone_done_this_round(self) -> bool:
        """全員がこのラウンドで行動完了したか"""
        for i in self._alive_players():
            p = self.players[i]
            if p.allin:
                continue
            if p.bet_this_round != self.current_bet:
                return False
            if not p.has_acted:
                return False
        return True
    
    def _advance_phase(self):
        """次のフェーズに進む"""
        # ラウンド終了処理
        for p in self.players:
            p.reset_for_new_round()
        self.current_bet = 0
        self.last_raiser = None
        
        # コミュニティカード追加
        if self.phase == "preflop":
            for _ in range(3):  # フロップ3枚
                self.community.append(self.deck.pop())
            self.phase = "flop"
        elif self.phase == "flop":
            self.community.append(self.deck.pop())  # ターン1枚
            self.phase = "turn"
        elif self.phase == "turn":
            self.community.append(self.deck.pop())  # リバー1枚
            self.phase = "river"
        elif self.phase == "river":
            self.phase = "showdown"
        
        self.current_index = self._next_index(-1)  # 左から探索
        if self.phase == "showdown":
            self._do_showdown()
    
    def _do_showdown(self):
        """ショーダウン処理"""
        contenders = self._alive_players()
        if len(contenders) == 0:
            self.message = "All folded."
            return
        
        # 役判定
        scores = []
        for i in contenders:
            cards7 = self.players[i].hand + self.community
            score, best5 = best_hand_7(cards7)
            scores.append((score, i))
        
        # 勝者決定
        scores.sort(reverse=True, key=lambda x: x[0])
        top_score = scores[0][0]
        winners = [i for (sc, i) in scores if sc == top_score]
        
        # ポット分配
        share = self.pot // len(winners)
        for i in winners:
            self.players[i].chips += share
        
        self.message = f"Showdown! Winners: {', '.join(self.players[i].name for i in winners)} (+{share} each)"
        self.pot = 0
        
        # 脱落処理
        for p in self.players:
            if p.chips <= 0:
                p.active = False
                p.folded = True
                p.allin = False
        
        # ゲーム終了判定
        active_count = sum(1 for p in self.players if p.active)
        player_alive = any(p.active for p in self.players if p.name == "You")
        self.game_over = not (player_alive and active_count >= 2)
    
    def legal_actions(self, player_index: int) -> List[str]:
        """プレイヤーが取れる合法的なアクション"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if player.chips <= 0:
            return ["check"]
        
        actions = []
        if to_call == 0:
            actions += ["check", "bet", "raise", "fold", "allin"]
        else:
            actions += ["call", "raise", "fold", "allin"]
        
        return actions
    
    def apply_action(self, player_index: int, action: str) -> bool:
        """アクションを適用"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if action == "fold":
            player.folded = True
            player.has_acted = True
            self.message = f"{player.name} folds."
            
        elif action == "check":
            if to_call != 0:
                self.message = f"{player.name} cannot check (must call {to_call})."
                return False
            player.has_acted = True
            self.message = f"{player.name} checks."
            
        elif action == "call":
            pay = min(to_call, player.chips)
            player.chips -= pay
            player.bet_this_round += pay
            self.pot += pay
            if player.chips == 0:
                player.allin = True
            player.has_acted = True
            self.message = f"{player.name} calls {pay}."
            
        elif action == "bet":
            if self.current_bet != 0:
                return self.apply_action(player_index, "raise")
            amount = min(10, player.chips)
            player.chips -= amount
            player.bet_this_round += amount
            self.pot += amount
            self.current_bet = player.bet_this_round
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} bets {amount}."
            
        elif action == "raise":
            if player.chips <= to_call:
                if player.chips < to_call:
                    return self.apply_action(player_index, "allin")
                else:
                    return self.apply_action(player_index, "call")
            call_amt = to_call
            raise_extra = min(10, player.chips - call_amt)
            total = call_amt + raise_extra
            player.chips -= total
            player.bet_this_round += total
            self.pot += total
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} raises to {player.bet_this_round}."
            
        elif action == "allin":
            need = max(0, self.current_bet - player.bet_this_round)
            all_amount = need + player.chips
            player.chips = 0
            player.bet_this_round += all_amount
            self.pot += all_amount
            player.allin = True
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} goes all-in ({player.bet_this_round})."
            
        else:
            self.message = "Unknown action."
            return False
        
        # 次のプレイヤーへ
        self.current_index = self._next_index(player_index)
        
        # ラウンド終了チェック
        alive_count = len(self._alive_players())
        if alive_count <= 1:
            # 残り1人なら即勝利
            self.phase = "showdown"
            winners = self._alive_players()
            if winners:
                winner = self.players[winners[0]]
                winner.chips += self.pot
                self.message = f"{winner.name} wins (others folded). +{self.pot}"
                self.pot = 0
            self._do_showdown()
            return True
        
        if self._everyone_done_this_round():
            self._advance_phase()
        
        return True
    
    def get_current_player(self) -> Optional[Player]:
        """現在の行動プレイヤー"""
        if self.current_index is None:
            return None
        return self.players[self.current_index]






bot_strategy.py
import random

RANK_ORDER = {r:i for i,r in enumerate(list("23456789TJQKA"), start=2)}

def approx_hand_strength_preflop(hand):
    # hand: ["As","Kd"] のような2枚
    r1, r2 = hand[0][0], hand[1][0]
    s1, s2 = hand[0][1], hand[1][1]
    pair = (r1 == r2)
    suited = (s1 == s2)
    v1, v2 = RANK_ORDER[r1], RANK_ORDER[r2]
    high = max(v1, v2)
    low  = min(v1, v2)

    score = 0
    if pair: score += 30 + high
    if suited: score += 5
    score += high/2 + low/5
    return score  # だいたい 10〜50 程度

def decide_strengthish_action(strength, to_call, chips, can_check):
    """ごく簡単な方針:
    - 強: レイズ/ベット or オールイン
    - 中: コール/チェック
    - 弱: フォールド(ただしチェックできるならチェック)
    """
    if chips <= 0:
        return "check" if can_check else "call"

    if strength >= 45:
        if chips <= to_call:  # ほぼショートならオールイン
            return "allin"
        return "raise" if to_call > 0 else "bet"
    elif strength >= 35:
        return "check" if can_check else ("call" if to_call <= chips else "fold")
    else:
        return "check" if can_check else ("fold" if to_call > 0 else "check")

class BotStrategy:
    def __init__(self, name):
        self.name = name

    def decide_action(self, hand, community_cards, chips, current_bet, bet_this_round):
        # to_call: ラウンド内で必要な追加額
        to_call = max(0, current_bet - bet_this_round)
        can_check = (to_call == 0)

        # 超簡易:プリフロップは上の関数、以降はランダム寄り + コール重視
        if len(community_cards) == 0:
            strength = approx_hand_strength_preflop(hand)
        else:
            # 盤面が出たらレンジ広めに
            strength = random.uniform(25, 50)

        action = decide_strengthish_action(strength, to_call, chips, can_check)

        # たまにブラフ/ミックス
        if action in ("check","call") and not can_check and random.random() < 0.07 and chips > to_call + 10:
            action = "raise"
        if action == "raise" and chips < to_call + 10:
            action = "call" if chips >= to_call else "allin"
        if action == "bet" and chips < 10:
            action = "allin"

        return action




3つのファイルをコピペして作って
下記のコマンドを実行すると動くと思います。

streamlit run main.py
スクリーンショット 2025-08-23 17.07.28


自分のターンの際に
Callなどのラジオボタンで行動選択
Submit Actionボタンで行動決定です。

1ゲーム終わったら
左上の「New Hand」で次のゲームへ
チップがなくなったら「Restart Game」でゲームリセット

一応、バグがちょろちょろ有るので
ちゃんとは動かないところもあるかもしれませんが
ちょっと試してみるは出来ると思います。


自分で自分のBOTと対戦するも良し
BOTを複数繋げて対戦させ、一番強いBOTを探るもよし
色々改造して遊べると思います。

今日はここまでです
それでは

どうやら口コミが消されている事件があるようなので
見てみることにしました。



解説動画はこちら






ジャングリア沖縄

2025年7月25日に開業したテーマパーク
「大自然没入型」を掲げ、恐竜や絶景などを
テーマにしたアトラクションや温泉施設を備える

口コミが香ばしいので、全部見てみました。



口コミデータを取得する方法

(Google Chromeの場合)
Google口コミから、ジャングリアの口コミを開く
最下段までスクロールする

その状態で「右クリックから検証」で検証ツールを開く
HTMLのソースをクリックして
「Copy outerHTML」を実行

テキストエディターに貼り付けて
htmlファイルとして保存する



HTMLをBeautiful Soup で解析する


クラス名の部分は変更される可能性があるので
データが取れない場合は、変更してみてください。
(HTMLファイル内のclassの部分の文字列に変更する)
from bs4 import BeautifulSoup

# ファイル名は適宜変更する
with open("google.html") as _f:
    html_data = _f.read()

soup = BeautifulSoup(html_data, "html.parser")
all_div = soup.find("div",class_="aSzfg")
divs = all_div.find_all("div",class_="bwb7ce")

data = []
for div in divs:
    date_tag = div.find("span",class_="y3Ibjb")
    date = date_tag.text.replace("最終編集: ","")
    
    name_tag = div.find("div",class_="Vpc5Fe")
    name = name_tag.text

    role_tag = div.find("div",class_="GSM50")
    roles = role_tag.text.split("·")
    role = roles[0] if len(roles)==3 else ""
    num = roles[1].replace(" 件のクチコミ","") if len(roles)==3 else roles[0].replace(" 件のクチコミ","")
    photo = roles[2].replace(" 枚の写真","") if len(roles)>2 else ("" if len(roles)==1 else roles[1].replace(" 枚の写真",""))
    
    rating_tag = div.find("div",class_="dHX2k")
    rating = rating_tag.get("aria-label").replace("5 点中 ","").replace(" 点の評価","")

    comment_tag = div.find("div",class_="OA1nbd")
    comment = comment_tag.text.replace(" …もっと見る","") if comment_tag is not None else ""
    tmp = [date,name,role,num,photo,rating,comment]
    data.append(tmp)

データフレームに変換する
import pandas as pd
import re

# 日付変換用関数
def convert_to_days(value):
    if "週間前" in value:
        weeks = int(re.search(r"(\d+)", value).group(1))
        return -weeks * 7
    elif "日前" in value:
        days = int(re.search(r"(\d+)", value).group(1))
        return -days
    elif "時間前" in value:
        return -1
    else:
        return 0

columns = ["日付", "名前", "職業", "口コミ数", "写真の数", "星の数", "コメント"]
df = pd.DataFrame(data,columns = columns)

df["口コミ数"] = df["口コミ数"].replace("", 0).astype(int)
df["写真の数"] = df["写真の数"].replace("", 0).astype(int)
df["星の数"] = df["星の数"].replace("", 0).astype(float)
df["日前"] = df["日付"].apply(convert_to_days)


星の数などを可視化する

データができたら可視化していきましょう

要ライブラリ
pip install japanize_matplotlib
pip install janome
pip install wordcloud


星の数ごとのカウント
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib

# ================
# 1. 星の数のヒストグラム
# ================
plt.figure(figsize=(6, 4))
sns.histplot(df["星の数"], bins=10, kde=True, color="purple")
plt.title("星の数のヒストグラム")
plt.xlabel("星の数")
plt.ylabel("件数")
plt.show()
download



散布図
# ================
# 2. 散布図 (2種類)
# ================
plt.figure(figsize=(12, 5))

# (1) 口コミ数 vs 星の数
plt.subplot(1, 3, 1)
sns.scatterplot(x="口コミ数", y="星の数", data=df, color="blue")
plt.title("口コミ数 vs 星の数")

# (2) 写真の数 vs 星の数
plt.subplot(1, 3, 2)
sns.scatterplot(x="写真の数", y="星の数", data=df, color="green")
plt.title("写真の数 vs 星の数")

# (3) X日前 vs 星の数
plt.subplot(1, 3, 3)
sns.scatterplot(x="日前", y="星の数", data=df, color="red")
plt.title("日前 vs 星の数")

plt.tight_layout()
plt.show()
download-2


日毎の星の数
# ▼ 棒グラフ作成
plt.figure(figsize=(12, 6))
sns.countplot(data=df, x="日前", hue="星の数", palette="viridis")

# ▼ 軸・タイトルの設定
plt.title("日前ごとの星の数カウント", fontsize=16)
plt.xlabel("日前", fontsize=14)
plt.ylabel("カウント数", fontsize=14)
plt.xticks(rotation=45)

plt.legend(title="星の数")
plt.tight_layout()
plt.show()
download-1



ワードクラウドでコメントを可視化する

フォントパスについては環境ごとに変更が必要です
下記はmacの例
import pandas as pd
from janome.tokenizer import Tokenizer
from wordcloud import WordCloud
import matplotlib.pyplot as plt

# Janome で形態素解析
tokenizer = Tokenizer()

def extract_nouns(text):
    """文章から名詞を抽出してリストで返す"""
    words = []
    for token in tokenizer.tokenize(text):
        part = token.part_of_speech.split(',')[0]
        if part == "名詞":
            words.append(token.surface)
    return words

# 全コメントから名詞を抽出
all_words = []
for comment in df["コメント"]:
    all_words.extend(extract_nouns(comment))
text_for_wc = " ".join(all_words)

# WordCloud 作成
wc = WordCloud(
    font_path="/System/Library/Fonts/ヒラギノ角ゴシック W6.ttc",
    width=800, height=600,
    background_color="white",
    colormap="viridis"
).generate(text_for_wc)

# 画像表示
plt.figure(figsize=(10, 8))
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.tight_layout()
plt.show()
download-3



終わりに

はじめにデータを取得した際は
63件ありました。

でも動画を撮る際には
58件に減っていました。

ここから言えることとしては

Google口コミが消されています!!!


Googleさん
口コミは重要なので消さないでください!!!

消した口コミ
元に戻してください

消費者が正しい判断ができるように
口コミは正しくあるべきです

良いものは良い
悪いものは悪い

悪いものが良いように操作されたり
その逆も絶対にあってはならんことです。

また、口コミを鵜呑みにせず
実際に見てみることも重要だったりします。

まあ、今回の件は
確実に消されているので
正しい口コミとして
判断できない状態になっています。

今後の展開が楽しみですね!!!
それでは。




このページのトップヘ