diff --git a/main_lever b/main_lever new file mode 100644 index 0000000..397358d --- /dev/null +++ b/main_lever @@ -0,0 +1,452 @@ +#include +#include +#include + +// ========================================================================= +// 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); + } + } +}