乙Py先生のプログラミング教室

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

今回はアプリをリリースしたので
そちらのご報告です。

解説動画はこちら



アプリについて


今回作成したアプリはこちらです

・チャンクde英会話

iOS
AppStore


Android
Googleplay

良かったら使ってみてください


アプリの作成方法

Flutterを用いて作成しました

Flutterにした理由は
 1.iOSとAndroid両対応であること
 2.学習コストが低い
 3.文献が豊富で有る

以上の理由からFlutterにしてみました。

それ以外にもアプリを作る方法は
いくらでもありますが、今回は
Flutterを使ってみました。

開発言語がDartになるので
1から勉強することになります。

また、コードやデータなどの大半は
ChatGPTを用いて作成していますので
実質ChatGPTに頼れば
アプリの開発は容易かと思います。

学習開発の期間で2ヶ月くらいでした。



リリースについて

コードを実装しシミュレータや
実機でのテストが終わったら
ビルドを行なってアプリを作り
リリース準備ができます。


その前にアプリストアのアカウントがないと
そもそもリリース出来ないので
アプリストアのアカウント取得が必要です。

これもそこそこ手間と時間が掛かります。

リリース登録をしたら審査が行われますが
審査に通らなければビルドからやり直し
審査に通るまでの繰り返しです。

審査に通ったらようやく
リリース、アプリ配信ができる様になります。

リリース作業を始めてから
アカウント登録とアプリリリースまでで
大体1ヶ月くらい掛かっています。



Flutterについて

動画の方では少しだけ解説していますが
FlutterはiOSとAndroidの両方のアプリを
作成する事ができる開発フレームワークです。

Flutterをインストールしたら
VSCodeなどでコードを書き進める事が
できる様になります。

テストやビルドなども
Flutterコマンドを用いて行う形になります。

この辺りも
VSCodeと合わせておくと
開発が楽になるかなと思います。

どんな感じなのかは
動画の方で解説していますので
参考にしていただければと思います。


最後に

これからアプリ開発を行いたい方にとっては
色々な選択肢があると思いますが
Flutterを使ってアプリを開発したい方が
増えていただけたら幸いです。

Python言語の解説と共に
アプリ開発の方も進めていきますので
要望などあれば是非コメントいただければと思います。

それでは。

あの伝説の国士無双13面待ち
これが出る確率を求めてみました。

解説動画はこちら


国士無双13面待ちが出る確率は?


今回は麻雀ネタです。

麻雀における最高峰の役「役満」の一つに
「国士無双」という役満があります

13種類の么九牌(一九字牌)を
各1枚以上集める特殊な形の役満です。


13面待ち
萬子・索子・筒子の1・9、東・南・西・北・白・發・中の
計13種類をすべて揃えた状態でアガリ待ちの状態のことです。

13



この13面待ちがどれくらいの確率でやってくるのか
プログラムで求めていきたいと思います。



1.接待麻雀モード

自分一人がずっと国士無双13面待ちを狙い
他はただパイを捨て続けるとした場合の確率です。



簡易に求めるコード
import random

# ヤオ九牌(1, 9, 字牌)のインデックス定義
YAOCHU_INDICES = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33]

def is_kokushi_13men_machi(tehai):
    """
    国士無双13面待ちの判定
    13種類のヤオ九牌がすべて1枚ずつ揃っているか
    """
    for idx in YAOCHU_INDICES:
        if tehai[idx] < 1:
            return False
    return True

def simulate_one_game_kokushi():
    # 山の作成(136枚)
    yama = []
    for i in range(34):
        yama.extend([i] * 4)
    random.shuffle(yama)

    # 全員の手牌(4人分)
    players_tehai = [[0] * 34 for _ in range(4)]
    
    # 配牌(各13枚)
    for p in range(4):
        for _ in range(13):
            pai = yama.pop()
            players_tehai[p][pai] += 1

    turn = 0
    while len(yama) > 14:
        turn += 1
        for p in range(4):
            # --- 1. ツモ ---
            tsumo_pai = yama.pop()
            players_tehai[p][tsumo_pai] += 1

            # --- 2. 判定(プレイヤー0のみ国士無双を判定) ---
            if p == 0 and is_kokushi_13men_machi(players_tehai[p]):
                return True, turn, p

            # --- 3. 捨て牌選択 ---
            if p == 0:
                # プレイヤー0: 国士無双を狙う戦略
                discard_candidates = []
                
                # 優先順位1: 中張牌を捨てる
                for i in range(34):
                    if i not in YAOCHU_INDICES and players_tehai[p][i] > 0:
                        discard_candidates.extend([i] * players_tehai[p][i])
                
                # 優先順位2: 被っているヤオ九牌を捨てる
                if not discard_candidates:
                    for i in YAOCHU_INDICES:
                        if players_tehai[p][i] >= 2:
                            discard_candidates.append(i)

                # 優先順位3: どれでも(13面待ち完成中など)
                if not discard_candidates:
                    discard_candidates = [i for i, count in enumerate(players_tehai[p]) if count > 0]
                
                discard_pai = random.choice(discard_candidates)
            else:
                # 他のプレイヤー: ツモ切り(今引いた牌をそのまま捨てる)
                discard_pai = tsumo_pai

            players_tehai[p][discard_pai] -= 1
            
            # 山がなくなったら終了(流局)
            if len(yama) <= 14:
                break
                
    return False, turn, None

def run_kokushi_experiment(episodes_per_step=10000):
    print(f"国士無双13面待ちシミュレーション ({episodes_per_step}局)\n")
    print("成立回数 | 成立確率 | 平均巡目")
    print("-----------------------------------------")

    success_count = 0
    total_turns = 0
        
    for _ in range(episodes_per_step):
        is_clear, turns, winner = simulate_one_game_kokushi()
        if is_clear:
            success_count += 1
            total_turns += turns
    
    prob = (success_count / episodes_per_step) * 100
    avg_turn = total_turns / success_count if success_count > 0 else 0
    print(f"{success_count:4}回  | {prob:6.2f}% | {avg_turn:5.2f}巡")

run_kokushi_experiment(100000)
国士無双13面待ちシミュレーション (100000局)

成立回数 | 成立確率 | 平均巡目
-----------------------------------------
 120回  |   0.12% | 16.44巡



接待麻雀モードになっていますが
10万局中120回
確率0.12%でした。



2.他家が確率でアガるモード

先ほどのプレイヤーがずっと国士無双を狙う状態で
他家が10巡目以降、確率でアガられてしまう状態の
確率を求めています。


3.13面待ちからアガる確率

他家が確率でアガるモードに加えて
13面待ちから、上がれたかどうかの
確率も求めました。

こちらはぜひ動画で確率をご覧ください。



まとめ

国士無双13面待ちは、それなりにアガるプレイヤーがいても
10000局に1回位は見れると推測されるので
年間300試合、試合平均11局だと、3-4年に1回は
13面待ちの実況が見れるかもしれません。

13面待ちでアガリの確率はさらに低いので(一生に一度出せるかどうか)
もう10年は見れないかもしれないですね。


今回は麻雀ネタでしたが
次回も多分麻雀ネタです。
それでは。



今回は名探偵コナンの
緋色の弾丸シミュレーションです。


解説動画はこちら




緋色の弾丸

『名探偵コナン 緋色の弾丸』

2021年4月16日公開のアニメ映画で
劇場版『名探偵コナン』シリーズの24作目です。

映画の中ではリニアの超高速移動に合わせて
数キロ以上の距離から、動く犯人の狙撃に成功しているシーンがあります

この弾丸の到達距離のシミュレーションを行なっていきます。


最長狙撃記録

現代の主要な最長狙撃記録です。
1位:約4,000m(2024年、ウクライナ軍、Snipex Alligator)
2位:3,800m(2023年、ウクライナ軍、MCR Horizon's Lord)
3位:3,540m(2017年、カナダ軍、TAC-50)

現実世界では3-4キロの到達が限界だと思われます。




映画上の設定

映画上には狙撃を成功させるための
重要なポイントとなる設定がいくつかありました。
世界初の「真空トンネル」内を走行する超電導リニアで時速は約1000km
ライフル弾には特注の「銀の弾丸」が使用された

リニアも弾丸も時速1000kmで飛んだ設定
リニアが減速した際に、ライフル弾は減速しないので、そのまま犯人に命中した

つまりは真空状態のリニア線路上を銀の弾丸が飛んでいったというのが
重要なポイントです。


銀の弾丸使用のポイント


狙撃用の弾の素材が銀が最良かどうかは分かりませんが
銀である必要はあったようです。


高い導電性:
銀は金属の中で電気を通しやすい

レンズの法則(渦電流):
導体である銀が、リニアの強力な変動磁場の中を高速で移動すると
弾丸表面に強力な「渦電流」が発生

磁気反発:
この渦電流が、レールの磁場と反発する磁力(ローレンツ力)を
生み出し、弾丸を浮かせる 「揚力」 となる


ということで
リニア線路内は重力の影響を受けない弾丸になる
と仮定することにします。


射撃シミュレーター

重力 0 や真空の状況を加味したものです

通常射撃(空気抵抗、重力影響有り)
真空射撃(空気抵抗 0、重力影響有り)
リニア射撃(空気抵抗 0、重力影響 0)
の状態を選べます。

コードはこちら
import numpy as np
import matplotlib.pyplot as plt
from math import pi, cos, sin, sqrt

class BallisticSimulator:
    def __init__(self, dt=0.01):
        self.dt = dt
        self.g = 9.80665

    def get_accelerations(self, vx, vy, k, am):
        """
        現在の速度と設定から、x方向とy方向の加速度を返す
        k: 空気抵抗係数, am: 磁気浮上加速度
        """
        v = sqrt(vx**2 + vy**2)
        # x方向: 空気抵抗のみ
        ax = -k * vx * v
        # y方向: 重力(下) + 磁気浮上(上) + 空気抵抗(上下)
        ay = -self.g + am - (k * vy * v)
        return ax, ay

    def simulate(self, mode="normal", v0_kmh=1000.0, deg=0.0, ht=1.5, mas_g=9.0, area_cm2=0.5):
        # モードごとの環境設定
        config = {
            "normal":      {"rho": 1.225, "am": 0.0},      # 1. 通常(空気あり、浮力なし)
            "vacuum":      {"rho": 0.0,   "am": 0.0},      # 2. 真空(空気なし、浮力なし)
            "maglev":      {"rho": 0.0,   "am": 9.80665}   # 3. 真空 + 磁気浮上
        }
        env = config.get(mode, config["normal"])
        v0 = v0_kmh / 3.6
        rad = deg * pi / 180
        # 物理定数の計算
        mas = mas_g / 1000.0
        s_area = area_cm2 / 10000.0
        cd = 0.3
        k = 0.5 * s_area * cd * env["rho"] / mas
        # 初期値
        x, y = 0.0, ht
        vx, vy = v0 * cos(rad), v0 * sin(rad)
        res_x, res_y = [x], [y]
        for _ in range(1000000):
            k1vx, k1vy = self.get_accelerations(vx, vy, k, env["am"])
            k2vx, k2vy = self.get_accelerations(vx + k1vx*self.dt/2, vy + k1vy*self.dt/2, k, env["am"])
            k3vx, k3vy = self.get_accelerations(vx + k2vx*self.dt/2, vy + k2vy*self.dt/2, k, env["am"])
            k4vx, k4vy = self.get_accelerations(vx + k3vx*self.dt, vy + k3vy*self.dt, k, env["am"])
            vx += (k1vx + 2*k2vx + 2*k3vx + k4vx) / 6 * self.dt
            vy += (k1vy + 2*k2vy + 2*k3vy + k4vy) / 6 * self.dt
            x += vx * self.dt
            y += vy * self.dt
            if y < 0 or x >= 300000: break
            res_x.append(x)
            res_y.append(y)
        return res_x, res_y

# --- 実行と可視化 ---
sim = BallisticSimulator()
modes = ["normal", "vacuum", "maglev"]
mode = modes[1]

rx, ry = sim.simulate(mode=mode, v0_kmh=1000.0, deg=0)
print(f"飛行時間  : {(len(rx) - 1) * sim.dt:.2f} 秒")
print(f"移動距離  : {rx[-1]:.2f} メートル, {rx[-1]/1000:.5f} キロメートル")
plt.figure(figsize=(16, 3))
plt.plot(rx, ry, label=f"Mode: {mode}")
plt.axhline(0, color='black', linewidth=1)
plt.title("Comparison of Bullet Trajectories (v=1000km/h)")
plt.xlabel("Distance (m)")
plt.ylabel("Height (m)")
plt.legend()
plt.grid(True)
plt.show()
飛行時間  : 0.54 秒
移動距離  : 150.00 メートル, 0.15000 キロメートル

download-3

 真空状態であったとしても
水平に撃ったのでは150メートルほどしか飛びません。


上の方に向けて撃った場合はもう少し長い距離飛びます。

download-2

射角5度くらいで1400メーターくらいです。
全然届かないですね。
しかも高さ30メートルの高低差が出てしまうので
リニアトンネルの天井にぶつかって終わってしまいます。


しかし、重力の影響が無いと仮定すると

download
銀の弾丸はまっすぐ飛んでいけます。



まとめ


弾丸が重力の影響を受けない限定的な条件下であれば
シミュレーション上ではトンネルが真っ直ぐ続く限り
銀の弾丸はどこまでも飛んでいくと推測されます。

狙撃距離は100kmあたり(2分後くらい)になると推測される

銀の弾丸に浮力が付き
リニアの線路がマジで真っ直ぐだったら
あり得ないでもない(赤井さんなら)
というのが結論です。


製作陣がどれだけの考慮をしたかは分かりませんが
特殊条件下であれば不可能でも無いと思います。

そういえば
来週からコナンの新作映画が公開されますね!!

速攻見にいきましょう


PS:
個人的には
逆襲のシャアの決着を
コナンで付けてもらいたかったのですが
もう叶わないのですかね






このページのトップヘ