#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);
}
}
}