#include <M5Unified.h>
#include <esp_now.h>
#include <WiFi.h>

// =========================================================================
// 1. 図柄・フラグ・ゲーム状態の定義
// =========================================================================
enum SymbolType { GRAPE=0, SEVEN=1, BAR=2, BELL=3, RHINO=4, CLOWN=5, CHERRY=6 };
const int TOTAL_SYMBOLS = 21;

enum InternalFlag {
    FLAG_BLANK=0, FLAG_REPLAY=1, FLAG_GRAPE=2, FLAG_CHERRY=3,
    FLAG_BELL=4, FLAG_CLOWN=5, FLAG_BIG=6, FLAG_REG=7
};

enum GameState {
    STATE_NORMAL,        // 通常時
    STATE_BONUS_FLAGGED, // ボーナス成立状態
    STATE_IN_BONUS       // ボーナス消化中
};

GameState current_state = STATE_NORMAL;
InternalFlag current_flag = FLAG_BLANK;
InternalFlag held_bonus = FLAG_BLANK; 

// --- メダル・リールウェイト管理用の変数 ---
int credit = 50;                  // Credit
int total_medals = 0;             // 総所持メダル
unsigned long last_lever_on_time = 0; // 前回のレバーON時刻
const unsigned long WEIGHT_TIME = 4100; // 4.1秒リールウェイト
int bonus_grape_count = 0;        

// =========================================================================
// 2. 全リールの図柄配列データ
// =========================================================================
const SymbolType reel_array[3][TOTAL_SYMBOLS] = {
    {BELL, SEVEN, RHINO, GRAPE, RHINO, GRAPE, BAR, CHERRY, GRAPE, RHINO, GRAPE, SEVEN, CLOWN, GRAPE, RHINO, GRAPE, CHERRY, BAR, GRAPE, RHINO, GRAPE},
    {RHINO, SEVEN, GRAPE, CHERRY, RHINO, BELL, GRAPE, CHERRY, RHINO, BAR, GRAPE, CHERRY, RHINO, BELL, GRAPE, CHERRY, RHINO, BAR, GRAPE, CHERRY, CLOWN},
    {GRAPE, SEVEN, BAR, BELL, RHINO, GRAPE, CLOWN, BELL, RHINO, GRAPE, CLOWN, BELL, RHINO, GRAPE, CLOWN, BELL, RHINO, GRAPE, CLOWN, BELL, RHINO}
};

// =========================================================================
// 3. 通信設定とリール位置記憶
// =========================================================================
typedef struct struct_main_to_reel {
    int command; 
    int slip_table[21];
} struct_main_to_reel;
struct_main_to_reel sendData;

typedef struct struct_reel_to_main {
    int reel_id;       
    int stopped_index; 
} struct_reel_to_main;
struct_reel_to_main receiveData;

uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
esp_now_peer_info_t peerInfo;

int pos[3] = {0, 0, 0};
const int slip_tables_first[3][8][21] = {
    // 左リールの第一停止テーブル
    {
        {2, 0, 1, 2, 2, 4, 3, 1, 2, 3, 4, 0, 1, 2, 2, 3, 3, 4, 3, 0, 1}, // BLANK
        {2, 2, 0, 1, 0, 1, 0, 2, 4, 4, 0, 1, 1, 3, 4, 4, 2, 2, 4, 0, 1}, // REPLAY
        {2, 1, 1, 2, 0, 1, 0, 2, 4, 4, 0, 1, 2, 0, 0, 0, 2, 4, 4, 0, 1}, // GRAPE
        {3, 4, 0, 4, 0, 4, 0, 0, 1, 2, 1, 4, 3, 4, 0, 0, 0, 1, 2, 1, 4}, // CHERRY
        {0, 1, 2, 2, 4, 4, 4, 2, 4, 4, 0, 1, 2, 1, 0, 0, 2, 4, 4, 0, 1}, // BELL
        {2, 1, 0, 4, 0, 4, 0, 2, 4, 4, 0, 1, 0, 1, 2, 3, 3, 3, 4, 0, 1}, // CLOWN
        {2, 0, 1, 2, 2, 4, 4, 4, 2, 3, 4, 0, 1, 2, 2, 3, 3, 4, 3, 0, 1}, // BIG
        {2, 0, 1, 2, 2, 4, 4, 4, 2, 3, 4, 0, 1, 2, 2, 3, 3, 4, 3, 0, 1}  // REG
    },
    // 中リールの第一停止テーブル(ソース見当たらずビタ止まり)
    {
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
    },
    // 右リールの第一停止テーブル
    {
        {4,0,1,2,3,4,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4}, {0,1,2,3,4,0,1,2,3,4,1,2,3,0,1,2,3,0,1,2,3},
        {0,0,0,2,4,3,4,1,3,2,0,1,3,2,0,0,2,0,1,0,2}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {4,0,1,2,2,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4}, {4,0,0,0,1,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4}
    }
};

// =========================================================================
// 4. GOGO!ランプ＆情報表示グラフィック
// =========================================================================
void draw_gogo_lamp(bool light_on) {
    int cx = M5.Display.width() / 2;
    int cy = M5.Display.height() / 2 + 20;

    if (!light_on) {
        M5.Display.fillRect(0, 60, M5.Display.width(), M5.Display.height() - 60, TFT_BLACK);
        return;
    }


    M5.Display.fillCircle(cx, cy, 38, M5.Display.color565(20, 20, 40)); 
    
    uint16_t flash_color = TFT_MAGENTA;
    for (int angle = 0; angle < 360; angle += 30) {
        float rad1 = (angle - 15) * DEG_TO_RAD;
        float rad2 = (angle + 15) * DEG_TO_RAD;
        float rad3 = angle * DEG_TO_RAD;
        
        int x1 = cx + cos(rad1) * 15;  int y1 = cy + sin(rad1) * 15;
        int x2 = cx + cos(rad2) * 15;  int y2 = cy + sin(rad2) * 15;
        int x3 = cx + cos(rad3) * 45;  int y3 = cy + sin(rad3) * 45;
        
        M5.Display.fillTriangle(x1, y1, x2, y2, x3, y3, flash_color);
    }

    for (int angle = 15; angle < 375; angle += 30) {
        float rad1 = (angle - 10) * DEG_TO_RAD;
        float rad2 = (angle + 10) * DEG_TO_RAD;
        float rad3 = angle * DEG_TO_RAD;
        int x1 = cx + cos(rad1) * 10;  int y1 = cy + sin(rad1) * 10;
        int x2 = cx + cos(rad2) * 10;  int y2 = cy + sin(rad2) * 10;
        int x3 = cx + cos(rad3) * 32;  int y3 = cy + sin(rad3) * 32;
        M5.Display.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_YELLOW);
    }

    M5.Display.setTextColor(M5.Display.color565(255, 255, 200)); // 輝く白黄色
    M5.Display.setTextSize(4);
    M5.Display.setTextDatum(MC_DATUM); // 中央揃え指定
    M5.Display.drawString("GOGO!", cx, cy);
}

void update_display() {
    draw_gogo_lamp(current_state == STATE_BONUS_FLAGGED);
    M5.Display.fillRect(0, 0, M5.Display.width(), 60, M5.Display.color565(30, 30, 30));
    M5.Display.setTextColor(TFT_GREEN, M5.Display.color565(30, 30, 30));
    M5.Display.setTextSize(2);
    M5.Display.setTextDatum(TL_DATUM);
    M5.Display.setCursor(10, 10);
    M5.Display.printf("CREDIT: %02d", credit);
    M5.Display.setCursor(10, 35);
    M5.Display.printf("POOL: %d", total_medals);

    // 3. ボーナス中の文字を灰色のエリアの下（Y座標70付近）に描画する
    if (current_state == STATE_IN_BONUS) {
        M5.Display.setTextColor(TFT_RED, TFT_BLACK); // 背景色を黒
        M5.Display.setTextSize(3);                   
        M5.Display.setCursor(10, 70);                
        M5.Display.printf("BONUS:%02d", bonus_grape_count);
    }
}

// =========================================================================
// 5. 滑り論理計算
// =========================================================================
void calculate_and_send_table(int target_id) {
    bool valid_stops[21]; 
    int lines[5][3] = {{1,1,1}, {2,2,2}, {0,0,0}, {2,1,0}, {0,1,2}}; 

    for (int i = 0; i < 21; i++) {
        bool is_ok = (current_flag == FLAG_BLANK) ? true : false; 
        
        SymbolType window[3] = {
            reel_array[target_id][i],
            reel_array[target_id][(i - 1 + TOTAL_SYMBOLS) % TOTAL_SYMBOLS],
            reel_array[target_id][(i - 2 + TOTAL_SYMBOLS) % TOTAL_SYMBOLS]
        };

        if (current_flag != FLAG_BLANK) {
            SymbolType target_sym = (SymbolType)-1;
            if (current_flag == FLAG_BIG) target_sym = SEVEN;
            else if (current_flag == FLAG_REG) target_sym = (target_id == 2) ? BAR : SEVEN;
            else if (current_flag == FLAG_GRAPE) target_sym = GRAPE;

            if (pos[0] == -1 && pos[1] == -1 && pos[2] == -1) {
                if (window[0] == target_sym || window[1] == target_sym || window[2] == target_sym) is_ok = true;
            } else {
                for (int l = 0; l < 5; l++) {
                    bool line_match = true;
                    for (int r = 0; r < 3; r++) {
                        if (pos[r] != -1) {
                            int r_idx = (pos[r] - lines[l][r] + TOTAL_SYMBOLS) % TOTAL_SYMBOLS;
                            if (reel_array[r][r_idx] != target_sym) line_match = false;
                        }
                    }
                    if (line_match && window[lines[l][target_id]] == target_sym) {
                        is_ok = true;
                    }
                }
            }
        } 
        else {
            if (target_id == 0 && (window[0] == CHERRY || window[1] == CHERRY || window[2] == CHERRY)) {
                is_ok = false;
            }
            
            for (int l = 0; l < 5; l++) {
                SymbolType line_sym = (SymbolType)-1;
                bool is_tenpai = true;
                int active_count = 0;

                for (int r = 0; r < 3; r++) {
                    if (pos[r] != -1) {
                        active_count++;
                        int r_idx = (pos[r] - lines[l][r] + TOTAL_SYMBOLS) % TOTAL_SYMBOLS;
                        if (line_sym == (SymbolType)-1) line_sym = reel_array[r][r_idx];
                        else if (reel_array[r][r_idx] != line_sym) is_tenpai = false;
                    }
                }

                if (active_count > 0 && is_tenpai) {
                    if (window[lines[l][target_id]] == line_sym) is_ok = false;
                    if (line_sym == SEVEN && target_id == 2 && window[lines[l][2]] == BAR) is_ok = false;
                }
            }
        }
        valid_stops[i] = is_ok; 
    }

    for (int push_idx = 0; push_idx < 21; push_idx++) {
        int slip = 0; 
        for (int s = 0; s < 5; s++) {
            if (valid_stops[(push_idx - s + TOTAL_SYMBOLS) % TOTAL_SYMBOLS]) {
                slip = s; 
                break; 
            }
        }
        sendData.slip_table[push_idx] = slip;
    }
    
    sendData.command = target_id;
    esp_now_send(broadcastAddress, (uint8_t *) &sendData, sizeof(sendData));
    delay(10);
}

// =========================================================================
// 6. 出目判定と払い出し
// =========================================================================
void evaluate_lines() {
    SymbolType matrix[3][3];
    for(int i=0; i<3; i++) {
        matrix[i][0] = reel_array[i][pos[i]]; 
        matrix[i][1] = reel_array[i][(pos[i] - 1 + TOTAL_SYMBOLS) % TOTAL_SYMBOLS]; 
        matrix[i][2] = reel_array[i][(pos[i] - 2 + TOTAL_SYMBOLS) % TOTAL_SYMBOLS]; 
    }

    int lines[5][3] = {{1,1,1}, {2,2,2}, {0,0,0}, {2,1,0}, {0,1,2}};
    bool is_big_aligned = false;  bool is_reg_aligned = false;  bool is_grape_aligned = false;

    for(int l=0; l<5; l++) {
        SymbolType s1 = matrix[0][lines[l][0]]; SymbolType s2 = matrix[1][lines[l][1]]; SymbolType s3 = matrix[2][lines[l][2]];
        if (s1 == SEVEN && s2 == SEVEN && s3 == SEVEN) is_big_aligned = true;
        if (s1 == SEVEN && s2 == SEVEN && s3 == BAR) is_reg_aligned = true;
        if (s1 == GRAPE && s2 == GRAPE && s3 == GRAPE) is_grape_aligned = true;
    }

    if (current_state == STATE_BONUS_FLAGGED) {
        if (held_bonus == FLAG_BIG && is_big_aligned) {
            current_state = STATE_IN_BONUS;
            bonus_grape_count = 20; 
            Serial.println("★★★ BIG BONUS START! ★★★");
        } else if (held_bonus == FLAG_REG && is_reg_aligned) {
            current_state = STATE_IN_BONUS;
            bonus_grape_count = 20;
            Serial.println("★★★ REG BONUS START! ★★★");
        }
    } 
    else if (current_state == STATE_IN_BONUS) {
        if (is_grape_aligned) {
            bonus_grape_count--;
            credit += 14; // ボーナス中ブドウは15枚払い出し（1枚掛けなので差引+14枚）
            if (credit > 50) { total_medals += (credit - 50); credit = 50; } // 50枚溢れ処理
            if (bonus_grape_count <= 0) current_state = STATE_NORMAL;
        }
    }
    else if (current_state == STATE_NORMAL) {
        if (is_grape_aligned) {
            credit += 7; // 通常時ブドウは7枚払い出し
            if (credit > 50) { total_medals += (credit - 50); credit = 50; }
        }
    }
    update_display();
}

// =========================================================================
// 7. 受信およびシステムループ
// =========================================================================
void OnDataRecv(const esp_now_recv_info *info, const uint8_t *incomingData, int len) {
    if (len == sizeof(struct_reel_to_main)) {
        memcpy(&receiveData, incomingData, sizeof(receiveData));
        pos[receiveData.reel_id] = receiveData.stopped_index;
        
        int spinning_count = 0;
        for (int i = 0; i < 3; i++) {
            if (pos[i] == -1) {
                calculate_and_send_table(i);
                spinning_count++;
            }
        }

        if (spinning_count == 0) {
            evaluate_lines();
        }
    }
}

void setup() {
    auto cfg = M5.config(); M5.begin(cfg); Serial.begin(115200);
    M5.Display.setRotation(0); M5.Display.fillScreen(TFT_BLACK); 
    WiFi.mode(WIFI_STA);
    if (esp_now_init() != ESP_OK) return;
    esp_now_register_recv_cb(OnDataRecv);
    memcpy(peerInfo.peer_addr, broadcastAddress, 6);
    peerInfo.channel = 0; peerInfo.encrypt = false;
    esp_now_add_peer(&peerInfo);

    update_display(); // 初期画面の描画
}

// =========================================================================
// 8. ゲーム開始（レバーON）共通処理関数
// =========================================================================
void start_game(int forced_flag = -1) {
    // リール回転中チェック
    if (pos[0] == -1 || pos[1] == -1 || pos[2] == -1) {
        Serial.println("エラー：リール回転中のためレバーONを受け付けません！");
        return; 
    }

    int bet_amount = (current_state == STATE_IN_BONUS) ? 1 : 3;

    // クレジット・プール処理
    if (credit < bet_amount && total_medals > 0) {
        int charge = 50 - credit;
        if (total_medals < charge) charge = total_medals;
        credit += charge;
        total_medals -= charge;
    }
    if (credit < bet_amount) {
        Serial.println("メダルが足りません！Bボタンでチャージしてください。");
        return; 
    }

    // ウェイト処理
    unsigned long now = millis();
    if (now - last_lever_on_time < WEIGHT_TIME) {
        unsigned long wait_needed = WEIGHT_TIME - (now - last_lever_on_time);
        Serial.printf("ウェイト発動: %d ms 待機します...\n", wait_needed);
        delay(wait_needed);
    }
    last_lever_on_time = millis();

    // メダル消費とリール状態リセット
    credit -= bet_amount;
    pos[0] = -1; pos[1] = -1; pos[2] = -1; 
    update_display();

    // フラグ決定（シリアル入力からの強制指定があれば優先）
    if (forced_flag != -1) {
        current_flag = (InternalFlag)forced_flag;
        // BIGやREGを強制指定した場合、内部状態も「ボーナス確定状態」へ強制移行させる
        if (current_flag == FLAG_BIG || current_flag == FLAG_REG) {
            current_state = STATE_BONUS_FLAGGED;
            held_bonus = current_flag;
        }
    } else {
        // 通常の乱数抽選
        マイジャグラー5の最高設定（設定6）の確率、めちゃくちゃ良いですね！
「1/229.1」という圧倒的なボーナス合算と、コイン持ちを良くする「1/5.66」のブドウ確率を、マイコンの乱数（0〜65535）の閾値に計算し直して完璧に再現しました。

また、今まで省略していた「REG（レギュラーボーナス）」の抽選も追加しています。これにより、GOGO!ランプが光った後に「7-7-BAR」が揃うルートも完全に機能するようになります！

指定された部分を、以下のコードにそっくりそのまま差し替えてください。

C++
        // 4. フラグ抽選（マイジャグラー5 設定6 確率）
        if (current_state == STATE_NORMAL) {
            uint16_t rand_val = esp_random() % 65536; 
            
            // 【確率の計算式 (分母65536)】
            // BIG    : 1/229.1 ≒ 286 / 65536
            // REG    : 1/229.1 ≒ 286 / 65536
            // ブドウ : 1/5.66  ≒ 11579 / 65536

            if (rand_val < 286) { 
                // 0 〜 285 なら BIG
                current_flag = FLAG_BIG; 
                current_state = STATE_BONUS_FLAGGED; 
                held_bonus = FLAG_BIG;               
            } else if (rand_val < 572) { 
                // 286 〜 571 なら REG (286 + 286)
                current_flag = FLAG_REG; 
                current_state = STATE_BONUS_FLAGGED; 
                held_bonus = FLAG_REG;               
            } else if (rand_val < 12151) {
                // 572 〜 12150 なら ブドウ (572 + 11579)
                current_flag = FLAG_GRAPE;
            } else {
                // それ以外はハズレ
                current_flag = FLAG_BLANK;
            }
        } 
        else if (current_state == STATE_BONUS_FLAGGED) {
            current_flag = held_bonus;
        }
        else if (current_state == STATE_IN_BONUS) {
            current_flag = FLAG_GRAPE; 
        }
    }
    
    Serial.printf("\n=== レバーON！ 消費:%d枚 フラグ: %d ===\n", bet_amount, current_flag);

    // リールへ送信
    sendData.command = 99;
    esp_now_send(broadcastAddress, (uint8_t *) &sendData, sizeof(sendData));
    delay(30);

    calculate_and_send_table(0);
    calculate_and_send_table(1);
    calculate_and_send_table(2);
}

// =========================================================================
// 9. メインループ
// =========================================================================
void loop() {
    M5.update();

    // --- 【Bボタン】メダルチャージ ---
    if (M5.BtnB.wasPressed()) {
        total_medals += 50;
        Serial.printf("メダルを50枚追加しました（POOL: %d）\n", total_medals);
        update_display();
    }

    // --- 【Aボタン】通常のレバーON ---
    if (M5.BtnA.wasPressed()) {
        start_game(-1); // -1 を渡すと通常の乱数抽選になる
    }

    // --- 【デバッグ用】強制フラグでのレバーON ---
    if (Serial.available() > 0) {
        char c = Serial.read();
        
        // 入力された文字が 0 〜 7 の数字だった場合
        if (c >= '0' && c <= '7') {
            int forced_flag = c - '0'; // 文字を数値に変換
            Serial.printf("\n【デバッグ】シリアルから強制フラグ [%d] が入力されました！\n", forced_flag);
            start_game(forced_flag);
        }
    }
}
