{"id":856428,"date":"2026-06-23T15:11:45","date_gmt":"2026-06-23T15:11:45","guid":{"rendered":"https:\/\/elektrobox.ch\/?page_id=856428"},"modified":"2026-07-20T17:29:44","modified_gmt":"2026-07-20T17:29:44","slug":"piggy-bank-slot-machine-esp32","status":"publish","type":"page","link":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/","title":{"rendered":"piggy-bank-slot-machine-esp32"},"content":{"rendered":"\n<div class=\"ebox-project-page ebox-project-page--piggy-slot\">\n\n  <section class=\"ebox-hero ebox-hero-with-image\">\n    <div class=\"ebox-hero-text\">\n      <span class=\"ebox-hero-label\">ESP32 Arcade Project<\/span>\n      <h1>Piggy Bank Slot Machine<\/h1>\n      <p>\n        Saving money is usually boring, so I wanted to turn it into a small arcade game. This project combines a piggy bank with a slot machine: every inserted coin gives one spin, small wins give extra spins, and the jackpot unlocks the money chamber.\n      <\/p>\n      <p>\n        The machine is built around an ESP32, a coin sensor, a spin button, a TFT display, and a servo-controlled gate. The main idea is simple: coins go in, the game starts, and the chamber only opens when the machine hits <strong>777<\/strong>.\n      <\/p>\n    <\/div>\n\n    <div class=\"ebox-hero-image\">\n      <img decoding=\"async\" src=\"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png\" alt=\"DIY piggy bank slot machine with ESP32, display, coin slot and servo gate\">\n      \n    <\/div>\n  <\/section>\n\n  <div class=\"ebox-video-card\">\n    <div class=\"ebox-video-frame\">\n      <iframe src=\"https:\/\/www.youtube.com\/embed\/Lrjl0_UwxDs?start=103\" title=\"Piggy Bank Slot Machine project video\" allowfullscreen=\"\"><\/iframe>\n    <\/div>\n  <\/div>\n\n  <h2>1. Project Idea<\/h2>\n  <p>\n    The goal of this project was to make saving coins more fun. Instead of just dropping money into a normal piggy bank, every coin gives the player one spin on a small slot machine. The player can win extra spins, but the money chamber stays locked until the jackpot result appears.\n  <\/p>\n\n  <div class=\"ebox-card-grid\">\n    <div class=\"ebox-info-card\"><strong>Coin inserted<\/strong>One coin is detected and one available spin is added.<\/div>\n    <div class=\"ebox-info-card\"><strong>Spin button<\/strong>The button starts the slot machine when at least one spin credit is available.<\/div>\n    <div class=\"ebox-info-card\"><strong>Small wins<\/strong>Winning combinations give extra spins, but do not open the money chamber.<\/div>\n    <div class=\"ebox-info-card\"><strong>Jackpot<\/strong>When the result is 777, the ESP32 opens the gate to the money chamber.<\/div>\n  <\/div>\n\n  <h2>2. Required Parts<\/h2>\n  <p>For the first working prototype, I used the following parts:<\/p>\n\n  <div class=\"ebox-card-grid\">\n    <div class=\"ebox-info-card\"><strong>Controller<\/strong>ESP32 development board<\/div>\n    <div class=\"ebox-info-card\"><strong>Display<\/strong>TFT display controlled with the TFT_eSPI library<\/div>\n    <div class=\"ebox-info-card\"><strong>Coin sensor<\/strong>TCST2103 optical light gate or similar IR slot sensor<\/div>\n    <div class=\"ebox-info-card\"><strong>Button<\/strong>Momentary push button for starting a spin<\/div>\n    <div class=\"ebox-info-card\"><strong>Gate motor<\/strong>Small servo motor for locking and unlocking the money chamber<\/div>\n    <div class=\"ebox-info-card\"><strong>Resistors<\/strong>220 \u03a9 for the IR LED and 10 k\u03a9 for the sensor signal circuit<\/div>\n    <div class=\"ebox-info-card\"><strong>Power<\/strong>5 V supply for the ESP32\/display setup and a suitable supply for the servo<\/div>\n    <div class=\"ebox-info-card\"><strong>Housing<\/strong>3D printed two-chamber enclosure with coin slot and front panel<\/div>\n  <\/div>\n\n  <div class=\"ebox-warning\">\n    <strong>Important:<\/strong> A servo should not be powered directly from a weak ESP32 3.3 V pin. Use a suitable 5 V supply for the servo and connect the ESP32 ground and servo supply ground together.\n  <\/div>\n\n  <h2>3. System Overview<\/h2>\n  <p>\n    The machine has three main jobs: detect an inserted coin, detect the spin button, and control the gate. The ESP32 connects these parts together and decides when the player is allowed to spin or open the money chamber.\n  <\/p>\n\n  <div class=\"ebox-flow\">\n    <div class=\"ebox-flow-step\">Coin passes through the optical sensor<\/div>\n    <div class=\"ebox-flow-step\">ESP32 adds one spin credit<\/div>\n    <div class=\"ebox-flow-step\">Button press starts the slot machine animation<\/div>\n    <div class=\"ebox-flow-step\">ESP32 checks the result and calculates the payout<\/div>\n    <div class=\"ebox-flow-step\">Only 777 activates the servo gate and opens the money chamber<\/div>\n  <\/div>\n\n  <h2>4. Coin Detection with an Optical Sensor<\/h2>\n  <div class=\"ebox-section-grid\">\n    <div>\n      <p>\n        The most important part of the project is the coin detection. I used a TCST2103 optical switch. This component works like a small light gate. One side contains an infrared LED, and the other side contains a phototransistor.\n      <\/p>\n      <p>\n        If there is no coin inside the slot, the infrared light reaches the phototransistor. When a coin falls through the slot, it blocks the light. The ESP32 reads this change as the coin signal.\n      <\/p>\n      <p>\n        In the prototype code below, the coin input is configured as an <strong>active-HIGH signal<\/strong>: the sensor reads LOW when the slot is free and HIGH while the coin blocks the light gate. This matches the uploaded prototype firmware.\n      <\/p>\n    <\/div>\n\n\n  <\/div>\n\n  <table class=\"ebox-table\">\n    <thead>\n      <tr>\n        <th>Condition<\/th>\n        <th>Sensor state in this prototype<\/th>\n        <th>ESP32 interpretation<\/th>\n      <\/tr>\n    <\/thead>\n    <tbody>\n      <tr>\n        <td>No coin in the light gate<\/td>\n        <td>LOW \/ free<\/td>\n        <td>No new coin counted<\/td>\n      <\/tr>\n      <tr>\n        <td>Coin blocks the light gate<\/td>\n        <td>HIGH \/ blocked<\/td>\n        <td>One coin is accepted and one spin credit is added<\/td>\n      <\/tr>\n    <\/tbody>\n  <\/table>\n\n  <div class=\"ebox-note\">\n    <strong>Wiring note:<\/strong> The video explanation and the prototype code must use the same signal logic. If your sensor circuit uses a pull-up version where the signal is inverted, change <code>COIN_SENSOR_ACTIVE_STATE<\/code> in the code or invert the condition in <code>readCoinSensorActive()<\/code>.\n  <\/div>\n\n  <h2>5. Spin Button<\/h2>\n  <p>\n    The spin button is wired from an ESP32 input pin to ground. In the firmware, the ESP32 internal pull-up resistor is enabled. That means the pin is normally HIGH. When the button is pressed, the pin is pulled to ground and becomes LOW.\n  <\/p>\n  <p>\n    The firmware detects this falling edge and starts the slot machine only if a spin credit is available. A short debounce time prevents one physical button press from being counted multiple times.\n  <\/p>\n\n  <div class=\"ebox-small-code-box\">\n    <textarea class=\"ebox-small-code\" readonly=\"\">#define SPIN_BUTTON_PIN          33\n#define SPIN_BUTTON_DEBOUNCE_MS  120\n\npinMode(SPIN_BUTTON_PIN, INPUT_PULLUP);<\/textarea>\n  <\/div>\n\n  <h2>6. Servo Gate for the Money Chamber<\/h2>\n  <div class=\"ebox-section-grid\">\n    <div>\n      <p>\n        The housing uses a two-chamber design. One side contains the electronics and the servo. The other side is the money chamber. The servo acts as a small gate that blocks access to the coins.\n      <\/p>\n      <p>\n        At startup, the servo is moved into the closed position. When the jackpot condition is reached, the ESP32 moves the servo to the open position for a short time and then closes it again.\n      <\/p>\n    <\/div>\n\n    \n  <\/div>\n\n  <div class=\"ebox-small-code-box\">\n    <textarea class=\"ebox-small-code\" readonly=\"\">#define GATE_SERVO_PIN           27\n#define GATE_SERVO_CLOSED_ANGLE  0\n#define GATE_SERVO_OPEN_ANGLE    90\n#define GATE_SERVO_OPEN_TIME_MS  2500<\/textarea>\n  <\/div>\n\n  <h2>7. Slot Machine Logic<\/h2>\n  <p>\n    The game logic is intentionally simple. Every spin creates three symbols on the display. The middle payline is the main result. If the symbols form a winning combination, the player gets extra spins. Only the jackpot opens the gate.\n  <\/p>\n\n  <table class=\"ebox-table\">\n    <thead>\n      <tr>\n        <th>Result<\/th>\n        <th>Reward<\/th>\n        <th>Gate opens?<\/th>\n      <\/tr>\n    <\/thead>\n    <tbody>\n      <tr>\n        <td>777<\/td>\n        <td>Jackpot<\/td>\n        <td>Yes<\/td>\n      <\/tr>\n      <tr>\n        <td>Other strong winning combinations<\/td>\n        <td>Extra spin credits<\/td>\n        <td>No<\/td>\n      <\/tr>\n      <tr>\n        <td>Small winning combinations<\/td>\n        <td>Small number of extra spins<\/td>\n        <td>No<\/td>\n      <\/tr>\n      <tr>\n        <td>No win<\/td>\n        <td>No extra credits<\/td>\n        <td>No<\/td>\n      <\/tr>\n    <\/tbody>\n  <\/table>\n\n  <div class=\"ebox-note\">\n    <strong>Design idea:<\/strong> Small wins keep the game fun, but they do not unlock the stored money. The real reward is only triggered by the jackpot result.\n  <\/div>\n\n  <h2>8. Prototype Electronics Test<\/h2>\n  <p>\n    Before putting everything into the housing, I tested the electronics on the bench. For the first prototype setup, the ESP32 was mainly used to provide power, read the coin signal, read the button input, drive the display, and control the servo gate.\n  <\/p>\n  <p>\n    I also reused my small hardware monitor to directly check whether the coin recognition and button input were working correctly. This made debugging much easier before the full mechanical assembly.\n  <\/p>\n\n  \n\n  <h2>9. Housing and Mechanical Assembly<\/h2>\n  <p>\n    After the electronics test, I started assembling the full prototype. The housing uses two chambers: the electronics chamber and the money chamber. The coin sensor sits directly below the coin slot, so every inserted coin must pass through the light gate before it drops into the storage area.\n  <\/p>\n  <p>\n    For this first version, I soldered the resistors and wires directly to the sensor leads instead of using a separate PCB. This is not the cleanest final solution, but it gives more freedom for cable routing inside a tight 3D printed housing.\n  <\/p>\n  <p>\n    Some parts of the 3D print were joined by carefully melting the PLA edges with an old soldering iron. This works for rough prototypes, but it should be done carefully because the visible surfaces can deform easily.\n  <\/p>\n\n  <div class=\"ebox-warning\">\n    <strong>Prototype warning:<\/strong> Melting PLA with a soldering iron is useful for quick tests, but it is not a clean final assembly method. Work with good ventilation and avoid touching hot plastic or fumes.\n  <\/div>\n\n  <h2>10. Display Rotation Issue<\/h2>\n  <p>\n    During assembly, I noticed that the display was mounted upside down. Luckily, this was only a software problem. The display output can be rotated by 180 degrees in the firmware, so the hardware did not need to be rebuilt.\n  <\/p>\n\n  <div class=\"ebox-small-code-box\">\n    <textarea class=\"ebox-small-code\" readonly=\"\">\/\/ Example for TFT_eSPI display rotation\n\/\/ Use the value that matches your physical display orientation.\ntft.setRotation(2);<\/textarea>\n  <\/div>\n\n  <h2>11. Final Prototype Result<\/h2>\n  <div class=\"ebox-final-box\">\n    <p>\n      This is the first working prototype of the piggy bank slot machine. A coin gives one spin, the button starts the slot machine, small wins give extra spins, and the jackpot result 777 opens the money chamber.\n    <\/p>\n    <p>\n      The prototype still needs improvements, especially in the housing design, cable management, and gate mechanism. But the basic idea works: the ESP32 can detect coins, manage spin credits, run the slot machine logic, and unlock the chamber only after a jackpot.\n    <\/p>\n    <p>\n      In the next version, I want to make the design cleaner, improve the gate mechanism, and build a more arcade-like front panel.\n    <\/p>\n\n    <div class=\"ebox-image-card\">\n      <img decoding=\"async\" src=\"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png\" alt=\"Final working prototype of the ESP32 piggy bank slot machine\" loading=\"lazy\">\n      \n    <\/div>\n  <\/div>\n\n  <h2>12. Pin Overview<\/h2>\n  <p>\n    These are the most important pins used in the uploaded prototype code. Change them if your wiring is different.\n  <\/p>\n\n  <table class=\"ebox-table\">\n    <thead>\n      <tr>\n        <th>Function<\/th>\n        <th>ESP32 Pin<\/th>\n        <th>Notes<\/th>\n      <\/tr>\n    <\/thead>\n    <tbody>\n      <tr>\n        <td>Coin sensor input<\/td>\n        <td>GPIO34<\/td>\n        <td>Input-only pin. The code expects an external signal circuit and active-HIGH coin detection.<\/td>\n      <\/tr>\n      <tr>\n        <td>Constant HIGH output for sensor circuit<\/td>\n        <td>GPIO32<\/td>\n        <td>Used as a permanent HIGH signal in the prototype wiring.<\/td>\n      <\/tr>\n      <tr>\n        <td>Spin button<\/td>\n        <td>GPIO33<\/td>\n        <td>Uses internal pull-up. Button connects the pin to GND when pressed.<\/td>\n      <\/tr>\n      <tr>\n        <td>Servo signal<\/td>\n        <td>GPIO27<\/td>\n        <td>Servo signal only. Use a suitable power supply for the servo.<\/td>\n      <\/tr>\n      <tr>\n        <td>TFT enable \/ backlight<\/td>\n        <td>GPIO17<\/td>\n        <td>Turns the display enable\/backlight signal on.<\/td>\n      <\/tr>\n      <tr>\n        <td>Touch CS \/ IRQ<\/td>\n        <td>GPIO26 \/ GPIO25<\/td>\n        <td>Included for future use, but not used as touch buttons in this prototype.<\/td>\n      <\/tr>\n    <\/tbody>\n  <\/table>\n\n  <details class=\"ebox-details\" open=\"\">\n    <summary>Full Prototype Code: piggybank.ino<\/summary>\n    <div class=\"ebox-details-content\">\n      <p>\n        This is the full Arduino prototype code used for the ESP32 piggy bank slot machine. Create a new Arduino IDE project, paste the code into the main <code>.ino<\/code> file, install the required libraries, and adjust the pin configuration if your wiring is different.\n      <\/p>\n\n      <div class=\"ebox-code-block\">\n        <div class=\"ebox-code-header\">\n          <span class=\"ebox-code-title\">piggybank.ino<\/span>\n          <button class=\"ebox-copy-button\" onclick=\"eboxCopyCode('piggy-slot-ino', this)\">Copy code<\/button>\n        <\/div>\n        <textarea id=\"piggy-slot-ino\" class=\"ebox-code-textarea large\" readonly=\"\">#include &lt;Arduino.h&gt;\n#include &lt;SPI.h&gt;\n#include &lt;Wire.h&gt;\n#include &lt;TFT_eSPI.h&gt;\n#include &lt;XPT2046_Touchscreen.h&gt;\n#include &lt;ESP32Servo.h&gt;\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Display\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nTFT_eSPI tft = TFT_eSPI();\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Touch configuration\n\/\/ Kept for future use, but no touch buttons are used in this version.\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define TOUCH_CS   26\n#define TOUCH_IRQ  25\nSPIClass touchSPI(HSPI);\nXPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ);\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Display enable \/ backlight\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define TFT_ENABLE_PIN 17\n#define TFT_ENABLE_ON  HIGH\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Coin sensor\n\/\/ GPIO34 is input-only on the ESP32. Use an external pulldown.\n\/\/ Sensor output: LOW when free, HIGH \/ 3.3 V while blocked by a coin.\n\/\/ GPIO32 is used as a constant HIGH output, for example to feed\n\/\/ the sensor side that needs a permanent 3.3 V HIGH signal.\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define USE_COIN_SENSOR 1\n#define COIN_SENSOR_PIN             34\n#define COIN_SENSOR_HIGH_PIN        32\n#define COIN_SENSOR_ACTIVE_STATE    HIGH\n\n#define COIN_INTERRUPT_LOCKOUT_MS  5     \/\/ ISR noise filter only\n#define COIN_COUNT_LOCKOUT_MS      350   \/\/ prevents one coin from being counted twice\n#define COIN_RELEASE_STABLE_MS     60    \/\/ sensor must be free this long before re-arming\n#define COIN_DEBUG_PRINT_MS        1000\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Start button with internal pull-up\n\/\/ Wiring: GPIO33 -&gt; button -&gt; GND\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define SPIN_BUTTON_PIN          33\n#define SPIN_BUTTON_DEBOUNCE_MS  120\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Jackpot gate servo\n\/\/ Servo signal: GPIO27 -&gt; servo signal wire\n\/\/ Servo power : external 5 V supply recommended\n\/\/ Important   : ESP32 GND and servo supply GND must be connected together\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define GATE_SERVO_PIN           27\n#define GATE_SERVO_CLOSED_ANGLE  0\n#define GATE_SERVO_OPEN_ANGLE    90\n#define GATE_SERVO_OPEN_TIME_MS  2500\n#define GATE_SERVO_MIN_US        500\n#define GATE_SERVO_MAX_US        2400\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Game settings\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n#define START_CREDITS 0\n#define SPIN_COST     1\n\n#define REEL_COUNT    3\n#define ROW_COUNT     3\n#define SYMBOL_COUNT  6\n\n#define REEL_SPEED_MS 75\n#define SPIN_ANIM_MS  90\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Slot symbols\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nenum SlotSymbol {\n  SYMBOL_CHERRY = 0,\n  SYMBOL_LEMON,\n  SYMBOL_SEVEN,\n  SYMBOL_BAR,\n  SYMBOL_BELL,\n  SYMBOL_DIAMOND\n};\n\nenum GameState {\n  STATE_IDLE,\n  STATE_SPINNING,\n  STATE_RESULT\n};\n\nenum WinLine {\n  WIN_NONE = 0,\n  WIN_TOP,\n  WIN_MIDDLE,\n  WIN_BOTTOM,\n  WIN_DIAG_DOWN,\n  WIN_DIAG_UP\n};\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Globals\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nGameState gameState = STATE_IDLE;\nWinLine activeWinLine = WIN_NONE;\n\nint credits = START_CREDITS;\nint totalCoinsSaved = 0;\n\nint reelSymbols[REEL_COUNT][ROW_COUNT];\nint finalSymbols[REEL_COUNT][ROW_COUNT];\nbool reelStopped[REEL_COUNT] = { false, false, false };\n\nunsigned long spinStartTime = 0;\nunsigned long lastReelUpdateTime = 0;\nunsigned long lastSpinFxTime = 0;\nuint8_t spinFxStep = 0;\n\nconst unsigned long reelStopTime[REEL_COUNT] = {\n  900, 1400, 1900\n};\n\nString messageText = &#8220;Insert coin&#8221;;\n\nbool gateClosePending = false;\nunsigned long gateOpenedTime = 0;\n\nServo gateServo;\n\n#if USE_COIN_SENSOR\nvolatile bool coinPulseDetected = false;\nvolatile unsigned long lastCoinInterruptUs = 0;\n#endif\n\n\/\/ Layout\nint screenW, screenH;\nint headerH;\nint gridX, gridY;\nint cellSize;\nint cellGap;\nint gridW, gridH;\nint messageY, messageH;\nint infoY, infoH;\n\n\/\/ Side decorative node positions\nconst int NODE_COUNT = 4;\nint leftNodeY[NODE_COUNT];\nint rightNodeY[NODE_COUNT];\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Function prototypes\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid setupLayout();\nvoid initReels();\n\nvoid drawMainScreen();\nvoid drawBackground();\nuint16_t bgColorAtY(int y);\nvoid fillBgRect(int x, int y, int w, int h);\n\nvoid drawElectroDecorStatic();\nvoid drawElectroDecorAnimated(bool spinning);\nvoid drawHeaderAnimated(bool spinning);\nvoid drawStatusLEDs(uint8_t step, bool spinning);\n\nvoid drawSlotFrame();\nvoid drawReels();\nvoid drawColumn(int col, bool highlight);\nvoid drawSingleCell(int col, int row, int symbol, bool highlight);\nvoid drawSymbol(int symbol, int x, int y, int w, int h);\nvoid drawCenterPaylineAnimated(bool spinning);\nvoid drawWinLine(WinLine line);\nvoid drawSpinIndicators(uint8_t step);\n\nvoid drawMessage(const String &amp;msg);\nvoid drawAnimatedSpinningMessage(uint8_t step);\nvoid drawInfoBar();\nvoid drawCenteredText(const String &amp;text, int x, int y, int size, uint16_t color, uint16_t bg);\n\nvoid startSpin();\nvoid updateSpin();\nvoid finishSpin();\nvoid addCoin();\nint randomSymbol();\nint payoutForThree(int a, int b, int c);\nint evaluatePayout(String &amp;resultMessage);\nbool is777JackpotWin();\nString lineName(WinLine line);\n\nvoid setupGateServo();\nvoid openGateServo();\nvoid closeGateServo();\nvoid updateGateServo();\n\nvoid IRAM_ATTR coinSensorISR();\nvoid updateCoinSensor();\nvoid updateSpinButton();\nbool readCoinSensorActive();\nvoid updateSpinAnimation();\n\nint cellX(int col);\nint cellY(int row);\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Setup\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid setup() {\n  Serial.begin(115200);\n  delay(500);\n\n  Serial.println();\n  Serial.println(&#8220;ESP32 Elektro Spin starting&#8230;&#8221;);\n\n  pinMode(TFT_ENABLE_PIN, OUTPUT);\n  digitalWrite(TFT_ENABLE_PIN, TFT_ENABLE_ON);\n\n  pinMode(SPIN_BUTTON_PIN, INPUT_PULLUP);\n\n  \/\/ Force the gate servo to the safe closed position during startup.\n  setupGateServo();\n\n#if USE_COIN_SENSOR\n  pinMode(COIN_SENSOR_HIGH_PIN, OUTPUT);\n  digitalWrite(COIN_SENSOR_HIGH_PIN, HIGH);\n\n  \/\/ GPIO34 has no internal pull-up\/pull-down.\n  \/\/ The pulldown must be external. Sensor signal is active HIGH.\n  pinMode(COIN_SENSOR_PIN, INPUT);\n\n  if (COIN_SENSOR_ACTIVE_STATE == HIGH) {\n    attachInterrupt(digitalPinToInterrupt(COIN_SENSOR_PIN), coinSensorISR, RISING);\n  } else {\n    attachInterrupt(digitalPinToInterrupt(COIN_SENSOR_PIN), coinSensorISR, FALLING);\n  }\n#endif\n\n  \/\/ Display\n  tft.init();\n  tft.setRotation(0);\n  tft.fillScreen(TFT_BLACK);\n\n  screenW = tft.width();\n  screenH = tft.height();\n\n  setupLayout();\n\n  \/\/ Touch init kept for future extensions.\n  touchSPI.begin(14, 12, 13, TOUCH_CS);  \/\/ SCK, MISO, MOSI, CS\n  ts.begin(touchSPI);\n  ts.setRotation(0);\n\n  randomSeed(micros());\n\n  initReels();\n  drawMainScreen();\n\n  Serial.println(&#8220;Ready&#8221;);\n  Serial.println(&#8220;Spin button: GPIO33 -&gt; button -&gt; GND&#8221;);\n  Serial.println(&#8220;Coin sensor : GPIO34 digital input, external pulldown, active HIGH&#8221;);\n  Serial.println(&#8220;GPIO32      : constant HIGH output&#8221;);\n  Serial.println(&#8220;Gate servo  : GPIO27 signal, starts at 0 degrees&#8221;);\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Main loop\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid loop() {\n  updateCoinSensor();\n  updateSpinButton();\n  updateGateServo();\n\n  if (gameState == STATE_SPINNING) {\n    updateSpin();\n\n    \/\/ Important:\n    \/\/ updateSpin() may finish the spin and change gameState to STATE_RESULT.\n    \/\/ Therefore only draw the spin animation if it is still spinning.\n    if (gameState == STATE_SPINNING) {\n      updateSpinAnimation();\n    }\n  }\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Layout\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid setupLayout() {\n  screenW = tft.width();\n  screenH = tft.height();\n\n  cellGap = 4;\n  headerH = 42;\n  messageH = 24;\n  infoH = 28;\n\n  int gridAreaTop = headerH + 8;\n  int infoBottomMargin = 6;\n\n  infoY = screenH &#8211; infoH &#8211; infoBottomMargin;\n  messageY = infoY &#8211; messageH &#8211; 5;\n\n  int gridAreaBottom = messageY &#8211; 8;\n  int availableH = gridAreaBottom &#8211; gridAreaTop;\n  int availableW = screenW &#8211; 26;\n\n  int maxCellByH = (availableH &#8211; 2 * cellGap) \/ 3;\n  int maxCellByW = (availableW &#8211; 2 * cellGap) \/ 3;\n\n  cellSize = min(maxCellByH, maxCellByW);\n  cellSize = constrain(cellSize, 38, 72);\n\n  gridW = 3 * cellSize + 2 * cellGap;\n  gridH = 3 * cellSize + 2 * cellGap;\n\n  gridX = (screenW &#8211; gridW) \/ 2;\n  gridY = gridAreaTop + (availableH &#8211; gridH) \/ 2;\n\n  leftNodeY[0] = gridY + 6;\n  leftNodeY[1] = gridY + gridH \/ 3;\n  leftNodeY[2] = gridY + (2 * gridH) \/ 3;\n  leftNodeY[3] = gridY + gridH &#8211; 6;\n\n  for (int i = 0; i &lt; NODE_COUNT; i++) {\n    rightNodeY[i] = leftNodeY[i];\n  }\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Helpers\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nint cellX(int col) {\n  return gridX + col * (cellSize + cellGap);\n}\n\nint cellY(int row) {\n  return gridY + row * (cellSize + cellGap);\n}\n\nvoid drawCenteredText(const String &amp;text, int x, int y, int size,\n                      uint16_t color, uint16_t bg) {\n  tft.setTextDatum(MC_DATUM);\n  tft.setTextSize(size);\n  tft.setTextColor(color, bg);\n  tft.drawString(text, x, y);\n  tft.setTextSize(1);\n}\n\nuint16_t bgColorAtY(int y) {\n  y = constrain(y, 0, screenH &#8211; 1);\n  float t = (float)y \/ (float)(screenH &#8211; 1);\n\n  uint8_t r = 8  + (int)((3  &#8211; 8)  * t);\n  uint8_t g = 20 + (int)((8  &#8211; 20) * t);\n  uint8_t b = 40 + (int)((18 &#8211; 40) * t);\n\n  return tft.color565(r, g, b);\n}\n\nvoid fillBgRect(int x, int y, int w, int h) {\n  if (w &lt;= 0 || h &lt;= 0) return;\n\n  x = constrain(x, 0, screenW &#8211; 1);\n  y = constrain(y, 0, screenH &#8211; 1);\n  w = min(w, screenW &#8211; x);\n  h = min(h, screenH &#8211; y);\n\n  for (int yy = y; yy &lt; y + h; yy++) {\n    tft.drawFastHLine(x, yy, w, bgColorAtY(yy));\n  }\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Visual theme\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid drawBackground() {\n  for (int y = 0; y &lt; screenH; y++) {\n    tft.drawFastHLine(0, y, screenW, bgColorAtY(y));\n  }\n}\n\nvoid drawElectroDecorStatic() {\n  uint16_t trace1 = tft.color565(0, 130, 160);\n  uint16_t trace2 = tft.color565(0, 100, 80);\n  uint16_t nodeDim = tft.color565(20, 90, 90);\n\n  int lx = 10;\n  tft.drawLine(lx, gridY &#8211; 6, lx, infoY + infoH, trace1);\n  tft.drawLine(lx, leftNodeY[0], 26, leftNodeY[0], trace1);\n  tft.drawLine(lx, leftNodeY[1], 22, leftNodeY[1], trace2);\n  tft.drawLine(lx, leftNodeY[2], 28, leftNodeY[2], trace1);\n  tft.drawLine(lx, leftNodeY[3], 24, leftNodeY[3], trace2);\n\n  for (int i = 0; i &lt; NODE_COUNT; i++) {\n    tft.fillCircle(lx, leftNodeY[i], 2, nodeDim);\n  }\n  tft.fillCircle(26, leftNodeY[0], 2, nodeDim);\n  tft.fillCircle(22, leftNodeY[1], 2, nodeDim);\n  tft.fillCircle(28, leftNodeY[2], 2, nodeDim);\n  tft.fillCircle(24, leftNodeY[3], 2, nodeDim);\n\n  int rx = screenW &#8211; 10;\n  tft.drawLine(rx, gridY &#8211; 6, rx, infoY + infoH, trace1);\n  tft.drawLine(rx &#8211; 16, rightNodeY[0], rx, rightNodeY[0], trace2);\n  tft.drawLine(rx &#8211; 22, rightNodeY[1], rx, rightNodeY[1], trace1);\n  tft.drawLine(rx &#8211; 18, rightNodeY[2], rx, rightNodeY[2], trace2);\n  tft.drawLine(rx &#8211; 24, rightNodeY[3], rx, rightNodeY[3], trace1);\n\n  for (int i = 0; i &lt; NODE_COUNT; i++) {\n    tft.fillCircle(rx, rightNodeY[i], 2, nodeDim);\n  }\n  tft.fillCircle(rx &#8211; 16, rightNodeY[0], 2, nodeDim);\n  tft.fillCircle(rx &#8211; 22, rightNodeY[1], 2, nodeDim);\n  tft.fillCircle(rx &#8211; 18, rightNodeY[2], 2, nodeDim);\n  tft.fillCircle(rx &#8211; 24, rightNodeY[3], 2, nodeDim);\n}\n\nvoid drawElectroDecorAnimated(bool spinning) {\n  if (!spinning) return;\n\n  uint16_t nodeDim = tft.color565(20, 90, 90);\n  uint16_t nodeBright1 = tft.color565(80, 255, 230);\n  uint16_t nodeBright2 = tft.color565(0, 220, 255);\n\n  int activeA = spinFxStep % NODE_COUNT;\n  int activeB = (spinFxStep + 2) % NODE_COUNT;\n\n  int lx = 10;\n  int rx = screenW &#8211; 10;\n\n  for (int i = 0; i &lt; NODE_COUNT; i++) {\n    tft.fillCircle(lx, leftNodeY[i], 2, nodeDim);\n    tft.fillCircle(rx, rightNodeY[i], 2, nodeDim);\n  }\n\n  tft.fillCircle(lx, leftNodeY[activeA], 3, nodeBright1);\n  tft.fillCircle(rx, rightNodeY[activeA], 3, nodeBright1);\n\n  tft.fillCircle(lx, leftNodeY[activeB], 2, nodeBright2);\n  tft.fillCircle(rx, rightNodeY[activeB], 2, nodeBright2);\n}\n\nvoid drawStatusLEDs(uint8_t step, bool spinning) {\n  uint16_t panel = tft.color565(5, 28, 55);\n\n  tft.fillCircle(18, 16, 5, panel);\n  tft.fillCircle(screenW &#8211; 18, 16, 5, panel);\n\n  uint16_t leftColor = TFT_RED;\n  uint16_t rightColor = TFT_GREEN;\n\n  if (spinning) {\n    leftColor = (step % 2 == 0) ? TFT_RED : TFT_ORANGE;\n    rightColor = (step % 2 == 0) ? TFT_GREEN : TFT_YELLOW;\n  }\n\n  tft.fillCircle(18, 16, 3, leftColor);\n  tft.fillCircle(screenW &#8211; 18, 16, 3, rightColor);\n}\n\nvoid drawHeaderAnimated(bool spinning) {\n  uint16_t panel = tft.color565(5, 28, 55);\n  uint16_t border = spinning ? tft.color565(0, 255, 220) : tft.color565(0, 220, 255);\n  uint16_t titleColor = spinning ? tft.color565(120, 255, 255) : tft.color565(180, 255, 255);\n\n  tft.fillRoundRect(8, 6, screenW &#8211; 16, headerH &#8211; 10, 8, panel);\n  tft.drawRoundRect(8, 6, screenW &#8211; 16, headerH &#8211; 10, 8, border);\n\n  drawStatusLEDs(spinFxStep, spinning);\n  drawCenteredText(&#8220;ELEKTRO SPIN&#8221;, screenW \/ 2, 21, 3, titleColor, panel);\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Slot frame and reel drawing\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid drawSlotFrame() {\n  uint16_t outer = tft.color565(0, 220, 255);\n  uint16_t inner = tft.color565(8, 16, 28);\n  uint16_t glow  = tft.color565(0, 120, 90);\n\n  int pad = 8;\n  tft.fillRoundRect(gridX &#8211; pad, gridY &#8211; pad, gridW + 2 * pad, gridH + 2 * pad, 10, inner);\n  tft.drawRoundRect(gridX &#8211; pad, gridY &#8211; pad, gridW + 2 * pad, gridH + 2 * pad, 10, outer);\n  tft.drawRoundRect(gridX &#8211; pad + 3, gridY &#8211; pad + 3, gridW + 2 * pad &#8211; 6, gridH + 2 * pad &#8211; 6, 8, glow);\n\n  for (int i = 0; i &lt; 3; i++) {\n    int yy = gridY + 10 + i * ((gridH &#8211; 20) \/ 2);\n    tft.drawLine(gridX &#8211; pad &#8211; 6, yy, gridX &#8211; pad, yy, outer);\n    tft.drawLine(gridX + gridW + pad, yy, gridX + gridW + pad + 6, yy, outer);\n  }\n}\n\nvoid drawSymbol(int symbol, int x, int y, int w, int h) {\n  int cx = x + w \/ 2;\n  int cy = y + h \/ 2;\n  int s = min(w, h);\n\n  switch (symbol) {\n    case SYMBOL_CHERRY: {\n      int r = max(4, s \/ 7);\n      tft.drawLine(cx, cy &#8211; r, cx + r, cy &#8211; 3 * r, TFT_GREEN);\n      tft.drawLine(cx &#8211; r, cy &#8211; r, cx + r, cy &#8211; 3 * r, TFT_GREEN);\n      tft.fillCircle(cx &#8211; r, cy + r, r, TFT_RED);\n      tft.fillCircle(cx + r, cy + r, r, TFT_RED);\n      tft.drawCircle(cx &#8211; r, cy + r, r, tft.color565(100, 0, 0));\n      tft.drawCircle(cx + r, cy + r, r, tft.color565(100, 0, 0));\n      break;\n    }\n\n    case SYMBOL_LEMON: {\n      int rw = max(8, s \/ 3);\n      int rh = max(5, s \/ 5);\n      tft.fillRoundRect(cx &#8211; rw, cy &#8211; rh, 2 * rw, 2 * rh, rh, TFT_YELLOW);\n      tft.drawRoundRect(cx &#8211; rw, cy &#8211; rh, 2 * rw, 2 * rh, rh, TFT_ORANGE);\n      int txtSize = (s &gt;= 44) ? 2 : 1;\n      drawCenteredText(&#8220;L&#8221;, cx, cy, txtSize, TFT_BLACK, TFT_YELLOW);\n      break;\n    }\n\n    case SYMBOL_SEVEN: {\n      int txtSize = (s &gt;= 56) ? 4 : (s &gt;= 42 ? 3 : 2);\n      drawCenteredText(&#8220;7&#8221;, cx, cy, txtSize, TFT_RED, TFT_WHITE);\n      break;\n    }\n\n    case SYMBOL_BAR: {\n      int bw = w &#8211; 10;\n      int bh = max(12, h \/ 3);\n      tft.fillRoundRect(cx &#8211; bw \/ 2, cy &#8211; bh \/ 2, bw, bh, 4, TFT_BLACK);\n      tft.drawRoundRect(cx &#8211; bw \/ 2, cy &#8211; bh \/ 2, bw, bh, 4, TFT_YELLOW);\n      int txtSize = (bw &gt;= 54) ? 2 : 1;\n      drawCenteredText(&#8220;BAR&#8221;, cx, cy, txtSize, TFT_WHITE, TFT_BLACK);\n      break;\n    }\n\n    case SYMBOL_BELL: {\n      int bw = max(18, s \/ 2);\n      int bh = max(18, s \/ 2);\n      uint16_t brown = tft.color565(120, 60, 0);\n      tft.fillRoundRect(cx &#8211; bw \/ 2, cy &#8211; bh \/ 2, bw, bh, 8, TFT_ORANGE);\n      tft.fillRect(cx &#8211; bw \/ 2 &#8211; 3, cy + bh \/ 4, bw + 6, max(4, bh \/ 5), TFT_ORANGE);\n      tft.fillCircle(cx, cy + bh \/ 2, 3, TFT_DARKGREY);\n      tft.drawRoundRect(cx &#8211; bw \/ 2, cy &#8211; bh \/ 2, bw, bh, 8, brown);\n      int txtSize = (s &gt;= 44) ? 2 : 1;\n      drawCenteredText(&#8220;B&#8221;, cx, cy, txtSize, TFT_BLACK, TFT_ORANGE);\n      break;\n    }\n\n    case SYMBOL_DIAMOND: {\n      int dh = max(10, s \/ 2);\n      int dw = max(8, s \/ 3);\n      uint16_t lightBlue = tft.color565(80, 210, 255);\n      tft.fillTriangle(cx, cy &#8211; dh, cx &#8211; dw, cy, cx + dw, cy, TFT_CYAN);\n      tft.fillTriangle(cx, cy + dh, cx &#8211; dw, cy, cx + dw, cy, lightBlue);\n      tft.drawLine(cx, cy &#8211; dh, cx &#8211; dw, cy, TFT_BLUE);\n      tft.drawLine(cx, cy &#8211; dh, cx + dw, cy, TFT_BLUE);\n      tft.drawLine(cx &#8211; dw, cy, cx, cy + dh, TFT_BLUE);\n      tft.drawLine(cx + dw, cy, cx, cy + dh, TFT_BLUE);\n      break;\n    }\n  }\n}\n\nvoid drawSingleCell(int col, int row, int symbol, bool highlight) {\n  int x = cellX(col);\n  int y = cellY(row);\n\n  uint16_t cellBg = tft.color565(244, 246, 248);\n  uint16_t border = highlight ? TFT_YELLOW : tft.color565(0, 160, 180);\n\n  tft.fillRoundRect(x, y, cellSize, cellSize, 6, cellBg);\n  tft.drawRoundRect(x, y, cellSize, cellSize, 6, border);\n\n  drawSymbol(symbol, x + 2, y + 2, cellSize &#8211; 4, cellSize &#8211; 4);\n}\n\nvoid drawColumn(int col, bool highlight) {\n  for (int row = 0; row &lt; ROW_COUNT; row++) {\n    drawSingleCell(col, row, reelSymbols[col][row], highlight);\n  }\n}\n\nvoid drawCenterPaylineAnimated(bool spinning) {\n  int y = cellY(1) + cellSize \/ 2;\n  uint16_t color = TFT_RED;\n\n  if (spinning) {\n    color = (spinFxStep % 2 == 0) ? TFT_RED : TFT_YELLOW;\n  }\n\n  tft.drawFastHLine(gridX &#8211; 4, y, gridW + 8, color);\n  tft.drawFastHLine(gridX &#8211; 4, y &#8211; 1, gridW + 8, tft.color565(120, 0, 0));\n}\n\nvoid drawReels() {\n  drawSlotFrame();\n\n  for (int col = 0; col &lt; REEL_COUNT; col++) {\n    drawColumn(col, false);\n  }\n\n  drawCenterPaylineAnimated(false);\n}\n\nvoid drawWinLine(WinLine line) {\n  if (line == WIN_NONE) return;\n\n  int x0, y0, x1, y1;\n\n  if (line == WIN_TOP) {\n    x0 = cellX(0) + 4;\n    y0 = cellY(0) + cellSize \/ 2;\n    x1 = cellX(2) + cellSize &#8211; 4;\n    y1 = y0;\n  } else if (line == WIN_MIDDLE) {\n    x0 = cellX(0) + 4;\n    y0 = cellY(1) + cellSize \/ 2;\n    x1 = cellX(2) + cellSize &#8211; 4;\n    y1 = y0;\n  } else if (line == WIN_BOTTOM) {\n    x0 = cellX(0) + 4;\n    y0 = cellY(2) + cellSize \/ 2;\n    x1 = cellX(2) + cellSize &#8211; 4;\n    y1 = y0;\n  } else if (line == WIN_DIAG_DOWN) {\n    x0 = cellX(0) + cellSize \/ 2;\n    y0 = cellY(0) + cellSize \/ 2;\n    x1 = cellX(2) + cellSize \/ 2;\n    y1 = cellY(2) + cellSize \/ 2;\n  } else {\n    x0 = cellX(0) + cellSize \/ 2;\n    y0 = cellY(2) + cellSize \/ 2;\n    x1 = cellX(2) + cellSize \/ 2;\n    y1 = cellY(0) + cellSize \/ 2;\n  }\n\n  tft.drawLine(x0, y0, x1, y1, TFT_YELLOW);\n  tft.drawLine(x0, y0 &#8211; 1, x1, y1 &#8211; 1, TFT_YELLOW);\n  tft.drawLine(x0, y0 + 1, x1, y1 + 1, TFT_ORANGE);\n}\n\nvoid drawSpinIndicators(uint8_t step) {\n  fillBgRect(gridX &#8211; 22, gridY &#8211; 4, 14, gridH + 8);\n  fillBgRect(gridX + gridW + 8, gridY &#8211; 4, 14, gridH + 8);\n\n  uint16_t c1 = (step % 2 == 0) ? TFT_CYAN : TFT_YELLOW;\n  uint16_t c2 = (step % 2 == 0) ? TFT_GREEN : TFT_ORANGE;\n\n  int offset = (step % 3) * (cellSize \/ 3);\n\n  for (int i = 0; i &lt; 3; i++) {\n    int yy = gridY + 10 + i * (gridH \/ 3) + offset \/ 3;\n    yy = constrain(yy, gridY + 8, gridY + gridH &#8211; 12);\n\n    tft.fillTriangle(gridX &#8211; 18, yy, gridX &#8211; 10, yy &#8211; 4, gridX &#8211; 10, yy + 4, c1);\n    tft.fillTriangle(gridX + gridW + 18, yy, gridX + gridW + 10, yy &#8211; 4, gridX + gridW + 10, yy + 4, c2);\n  }\n}\n\nvoid drawMessage(const String &amp;msg) {\n  uint16_t bg = tft.color565(4, 12, 22);\n  uint16_t border = tft.color565(0, 180, 200);\n\n  tft.fillRoundRect(12, messageY, screenW &#8211; 24, messageH, 6, bg);\n  tft.drawRoundRect(12, messageY, screenW &#8211; 24, messageH, 6, border);\n\n  tft.setTextDatum(MC_DATUM);\n  tft.setTextColor(TFT_WHITE, bg);\n  tft.setTextSize(2);\n  tft.drawString(msg, screenW \/ 2, messageY + messageH \/ 2);\n  tft.setTextSize(1);\n}\n\nvoid drawAnimatedSpinningMessage(uint8_t step) {\n  String txt = &#8220;SPINNING&#8221;;\n  uint8_t dots = step % 4;\n\n  for (uint8_t i = 0; i &lt; dots; i++) {\n    txt += &#8220;.&#8221;;\n  }\n\n  drawMessage(txt);\n}\n\nvoid drawInfoBar() {\n  uint16_t bg = tft.color565(3, 9, 16);\n  uint16_t border = tft.color565(0, 140, 110);\n  uint16_t txt = tft.color565(170, 255, 220);\n\n  tft.fillRoundRect(12, infoY, screenW &#8211; 24, infoH, 6, bg);\n  tft.drawRoundRect(12, infoY, screenW &#8211; 24, infoH, 6, border);\n\n  tft.setTextDatum(ML_DATUM);\n  tft.setTextColor(txt, bg);\n  tft.setTextSize(2);\n\n  String left = &#8220;Credits &#8221; + String(credits);\n  String right = &#8220;Saved &#8221; + String(totalCoinsSaved);\n\n  tft.drawString(left, 18, infoY + infoH \/ 2);\n  tft.drawString(right, screenW \/ 2 + 6, infoY + infoH \/ 2);\n\n  tft.setTextSize(1);\n}\n\nvoid drawMainScreen() {\n  drawBackground();\n  drawElectroDecorStatic();\n  drawHeaderAnimated(false);\n  drawReels();\n  drawMessage(messageText);\n  drawInfoBar();\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Spin animation only. No idle animation is used.\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid updateSpinAnimation() {\n  if (millis() &#8211; lastSpinFxTime &lt; SPIN_ANIM_MS) return;\n\n  lastSpinFxTime = millis();\n  spinFxStep++;\n\n  drawHeaderAnimated(true);\n  drawElectroDecorAnimated(true);\n  drawCenterPaylineAnimated(true);\n  drawSpinIndicators(spinFxStep);\n  drawAnimatedSpinningMessage(spinFxStep);\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Game logic\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid initReels() {\n  for (int col = 0; col &lt; REEL_COUNT; col++) {\n    for (int row = 0; row &lt; ROW_COUNT; row++) {\n      reelSymbols[col][row] = random(0, SYMBOL_COUNT);\n      finalSymbols[col][row] = reelSymbols[col][row];\n    }\n  }\n\n  reelSymbols[0][1] = SYMBOL_CHERRY;\n  reelSymbols[1][1] = SYMBOL_SEVEN;\n  reelSymbols[2][1] = SYMBOL_BAR;\n}\n\nint randomSymbol() {\n  int r = random(0, 100);\n\n  if (r &lt; 25) return SYMBOL_CHERRY;\n  if (r &lt; 43) return SYMBOL_LEMON;\n  if (r &lt; 60) return SYMBOL_BELL;\n  if (r &lt; 76) return SYMBOL_BAR;\n  if (r &lt; 92) return SYMBOL_DIAMOND;\n  return SYMBOL_SEVEN;\n}\n\nvoid addCoin() {\n  credits++;\n  totalCoinsSaved++;\n\n  messageText = &#8220;Coin detected&#8221;;\n  drawMessage(messageText);\n  drawInfoBar();\n\n  Serial.println(&#8220;Coin detected&#8221;);\n}\n\nvoid startSpin() {\n  if (gameState == STATE_SPINNING) return;\n\n  if (credits &lt; SPIN_COST) {\n    messageText = &#8220;Insert coin first&#8221;;\n    drawMessage(messageText);\n    return;\n  }\n\n  credits -= SPIN_COST;\n  activeWinLine = WIN_NONE;\n\n  for (int col = 0; col &lt; REEL_COUNT; col++) {\n    for (int row = 0; row &lt; ROW_COUNT; row++) {\n      finalSymbols[col][row] = randomSymbol();\n    }\n    reelStopped[col] = false;\n  }\n\n  gameState = STATE_SPINNING;\n  spinStartTime = millis();\n  lastReelUpdateTime = 0;\n  lastSpinFxTime = 0;\n  spinFxStep = 0;\n\n  drawReels();\n  drawInfoBar();\n  drawAnimatedSpinningMessage(0);\n\n  Serial.println(&#8220;Spin started&#8221;);\n}\n\nint payoutForThree(int a, int b, int c) {\n  if (!(a == b &amp;&amp; b == c)) return 0;\n\n  if (a == SYMBOL_SEVEN)   return 20;\n  if (a == SYMBOL_DIAMOND) return 8;\n  if (a == SYMBOL_BAR)     return 6;\n  if (a == SYMBOL_BELL)    return 8;\n  if (a == SYMBOL_LEMON)   return 4;\n  if (a == SYMBOL_CHERRY)  return 4;\n\n  return 0;\n}\n\nString lineName(WinLine line) {\n  switch (line) {\n    case WIN_TOP:       return &#8220;TOP&#8221;;\n    case WIN_MIDDLE:    return &#8220;MIDDLE&#8221;;\n    case WIN_BOTTOM:    return &#8220;BOTTOM&#8221;;\n    case WIN_DIAG_DOWN: return &#8220;DIAGONAL&#8221;;\n    case WIN_DIAG_UP:   return &#8220;DIAGONAL&#8221;;\n    default:            return &#8220;&#8221;;\n  }\n}\n\nbool is777JackpotWin() {\n  \/\/ Gate opens only for a real 777 line, not for other payouts.\n  if (reelSymbols[0][0] == SYMBOL_SEVEN &amp;&amp; reelSymbols[1][0] == SYMBOL_SEVEN &amp;&amp; reelSymbols[2][0] == SYMBOL_SEVEN) return true;\n  if (reelSymbols[0][1] == SYMBOL_SEVEN &amp;&amp; reelSymbols[1][1] == SYMBOL_SEVEN &amp;&amp; reelSymbols[2][1] == SYMBOL_SEVEN) return true;\n  if (reelSymbols[0][2] == SYMBOL_SEVEN &amp;&amp; reelSymbols[1][2] == SYMBOL_SEVEN &amp;&amp; reelSymbols[2][2] == SYMBOL_SEVEN) return true;\n  if (reelSymbols[0][0] == SYMBOL_SEVEN &amp;&amp; reelSymbols[1][1] == SYMBOL_SEVEN &amp;&amp; reelSymbols[2][2] == SYMBOL_SEVEN) return true;\n  if (reelSymbols[0][2] == SYMBOL_SEVEN &amp;&amp; reelSymbols[1][1] == SYMBOL_SEVEN &amp;&amp; reelSymbols[2][0] == SYMBOL_SEVEN) return true;\n\n  return false;\n}\n\nint evaluatePayout(String &amp;resultMessage) {\n  int bestPayout = 0;\n  WinLine bestLine = WIN_NONE;\n\n  int p = payoutForThree(reelSymbols[0][0], reelSymbols[1][0], reelSymbols[2][0]);\n  if (p &gt; bestPayout) { bestPayout = p; bestLine = WIN_TOP; }\n\n  p = payoutForThree(reelSymbols[0][1], reelSymbols[1][1], reelSymbols[2][1]);\n  if (p &gt; bestPayout) { bestPayout = p; bestLine = WIN_MIDDLE; }\n\n  p = payoutForThree(reelSymbols[0][2], reelSymbols[1][2], reelSymbols[2][2]);\n  if (p &gt; bestPayout) { bestPayout = p; bestLine = WIN_BOTTOM; }\n\n  p = payoutForThree(reelSymbols[0][0], reelSymbols[1][1], reelSymbols[2][2]);\n  if (p &gt; bestPayout) { bestPayout = p; bestLine = WIN_DIAG_DOWN; }\n\n  p = payoutForThree(reelSymbols[0][2], reelSymbols[1][1], reelSymbols[2][0]);\n  if (p &gt; bestPayout) { bestPayout = p; bestLine = WIN_DIAG_UP; }\n\n  activeWinLine = bestLine;\n\n  if (bestPayout &gt; 0) {\n    if (bestPayout &gt;= 20) {\n      resultMessage = &#8220;JACKPOT &#8221; + lineName(bestLine) + &#8221; +&#8221; + String(bestPayout);\n    } else {\n      resultMessage = &#8220;WIN &#8221; + lineName(bestLine) + &#8221; +&#8221; + String(bestPayout);\n    }\n    return bestPayout;\n  }\n\n  int cherryCount = 0;\n  if (reelSymbols[0][1] == SYMBOL_CHERRY) cherryCount++;\n  if (reelSymbols[1][1] == SYMBOL_CHERRY) cherryCount++;\n  if (reelSymbols[2][1] == SYMBOL_CHERRY) cherryCount++;\n\n  if (cherryCount == 2) {\n    activeWinLine = WIN_MIDDLE;\n    resultMessage = &#8220;Two cherries +1&#8221;;\n    return 1;\n  }\n\n  resultMessage = &#8220;No win&#8221;;\n  return 0;\n}\n\nvoid finishSpin() {\n  String resultMessage;\n  int payout = evaluatePayout(resultMessage);\n\n  credits += payout;\n  messageText = resultMessage;\n\n  drawReels();\n  drawWinLine(activeWinLine);\n  drawMessage(messageText);\n  drawInfoBar();\n  drawHeaderAnimated(false);\n  drawElectroDecorStatic();\n\n  if (is777JackpotWin()) {\n    openGateServo();\n  }\n\n  gameState = STATE_RESULT;\n\n  Serial.print(&#8220;Result: &#8220;);\n  Serial.print(resultMessage);\n  Serial.print(&#8221; | Payout: &#8220;);\n  Serial.println(payout);\n}\n\nvoid updateSpin() {\n  unsigned long now = millis();\n  unsigned long elapsed = now &#8211; spinStartTime;\n\n  if (now &#8211; lastReelUpdateTime &lt; REEL_SPEED_MS) return;\n  lastReelUpdateTime = now;\n\n  bool allStopped = true;\n\n  for (int col = 0; col &lt; REEL_COUNT; col++) {\n    if (!reelStopped[col]) {\n      if (elapsed &gt;= reelStopTime[col]) {\n        for (int row = 0; row &lt; ROW_COUNT; row++) {\n          reelSymbols[col][row] = finalSymbols[col][row];\n        }\n        reelStopped[col] = true;\n        drawColumn(col, true);\n      } else {\n        for (int row = 0; row &lt; ROW_COUNT; row++) {\n          reelSymbols[col][row] = randomSymbol();\n        }\n        drawColumn(col, false);\n      }\n    }\n\n    if (!reelStopped[col]) {\n      allStopped = false;\n    }\n  }\n\n  drawCenterPaylineAnimated(true);\n\n  if (allStopped) {\n    finishSpin();\n  }\n}\n\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Jackpot gate servo\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid setupGateServo() {\n  gateServo.setPeriodHertz(50);\n  gateServo.attach(GATE_SERVO_PIN, GATE_SERVO_MIN_US, GATE_SERVO_MAX_US);\n\n  \/\/ Force the gate into the safe closed position at startup.\n  gateServo.write(GATE_SERVO_CLOSED_ANGLE);\n  gateClosePending = false;\n\n  Serial.println(&#8220;Gate servo set to 0 degrees at startup&#8221;);\n}\n\nvoid openGateServo() {\n  gateServo.write(GATE_SERVO_OPEN_ANGLE);\n  gateOpenedTime = millis();\n  gateClosePending = true;\n  Serial.println(&#8220;777 jackpot: gate servo opened&#8221;);\n}\n\nvoid closeGateServo() {\n  gateServo.write(GATE_SERVO_CLOSED_ANGLE);\n  gateClosePending = false;\n  Serial.println(&#8220;Gate servo closed \/ 0 degrees&#8221;);\n}\n\nvoid updateGateServo() {\n  if (!gateClosePending) return;\n\n  if (millis() &#8211; gateOpenedTime &gt;= GATE_SERVO_OPEN_TIME_MS) {\n    closeGateServo();\n  }\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Coin sensor\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nbool readCoinSensorActive() {\n  return digitalRead(COIN_SENSOR_PIN) == COIN_SENSOR_ACTIVE_STATE;\n}\n\nvoid IRAM_ATTR coinSensorISR() {\n#if USE_COIN_SENSOR\n  unsigned long nowUs = micros();\n  unsigned long lockoutUs = COIN_INTERRUPT_LOCKOUT_MS * 1000UL;\n\n  \/\/ Capture only the active edge. This ISR must stay very small.\n  \/\/ The main loop decides whether the coin input is currently armed.\n  if (nowUs &#8211; lastCoinInterruptUs &gt;= lockoutUs) {\n    lastCoinInterruptUs = nowUs;\n    coinPulseDetected = true;\n  }\n#endif\n}\n\nvoid updateCoinSensor() {\n#if USE_COIN_SENSOR\n  static unsigned long lastPrintTime = 0;\n\n  \/\/ Armed\/released logic:\n  \/\/ 1. Count the first active edge.\n  \/\/ 2. Ignore every further edge while the same coin is still passing.\n  \/\/ 3. Re-arm only after the sensor is free for a stable time and the\n  \/\/    coin-count lockout has expired.\n  static bool coinArmed = true;\n  static unsigned long inactiveSince = 0;\n  static unsigned long lastAcceptedCoinMs = 0;\n\n  unsigned long now = millis();\n  bool sensorActive = readCoinSensorActive();\n\n  if (sensorActive) {\n    inactiveSince = 0;\n  } else {\n    if (inactiveSince == 0) {\n      inactiveSince = now;\n    }\n\n    if ((now &#8211; inactiveSince &gt;= COIN_RELEASE_STABLE_MS) &amp;&amp;\n        (now &#8211; lastAcceptedCoinMs &gt;= COIN_COUNT_LOCKOUT_MS)) {\n      coinArmed = true;\n    }\n  }\n\n  bool edgeDetected = false;\n\n  noInterrupts();\n  if (coinPulseDetected) {\n    coinPulseDetected = false;\n    edgeDetected = true;\n  }\n  interrupts();\n\n  if (edgeDetected &amp;&amp; coinArmed &amp;&amp;\n      (now &#8211; lastAcceptedCoinMs &gt;= COIN_COUNT_LOCKOUT_MS)) {\n    addCoin();\n    lastAcceptedCoinMs = now;\n    coinArmed = false;\n    inactiveSince = 0;\n    Serial.println(&#8220;Coin accepted: first active edge counted&#8221;);\n  } else if (edgeDetected) {\n    Serial.println(&#8220;Coin edge ignored: waiting for release \/ lockout&#8221;);\n  }\n\n  if (now &#8211; lastPrintTime &gt; COIN_DEBUG_PRINT_MS) {\n    lastPrintTime = now;\n    Serial.print(&#8220;Coin sensor state: &#8220;);\n    Serial.print(sensorActive ? &#8220;HIGH \/ blocked&#8221; : &#8220;LOW \/ free&#8221;);\n    Serial.print(&#8221; | armed: &#8220;);\n    Serial.println(coinArmed ? &#8220;YES&#8221; : &#8220;NO&#8221;);\n  }\n#endif\n}\n\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\n\/\/ Start button\n\/\/ &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;\nvoid updateSpinButton() {\n  static bool lastState = HIGH;\n  static unsigned long lastPressTime = 0;\n\n  bool state = digitalRead(SPIN_BUTTON_PIN);\n\n  if (lastState == HIGH &amp;&amp; state == LOW) {\n    if (millis() &#8211; lastPressTime &gt; SPIN_BUTTON_DEBOUNCE_MS) {\n      startSpin();\n      lastPressTime = millis();\n      Serial.println(&#8220;Start button pressed&#8221;);\n    }\n  }\n\n  lastState = state;\n}\n<\/textarea>\n      <\/div>\n    <\/div>\n  <\/details>\n\n  <details class=\"ebox-details\">\n    <summary>Code Explanation<\/summary>\n    <div class=\"ebox-details-content\">\n      <h3>Libraries<\/h3>\n      <p>\n        The code uses <code>TFT_eSPI<\/code> for the display, <code>XPT2046_Touchscreen<\/code> for future touch support, and <code>ESP32Servo<\/code> for controlling the gate servo.\n      <\/p>\n\n      <h3>Coin Input<\/h3>\n      <p>\n        The coin sensor is connected to <code>GPIO34<\/code>. The code expects the signal to become HIGH when the coin blocks the light gate. An interrupt captures the active edge, while the main loop handles lockout and re-arming so that one coin is not counted twice.\n      <\/p>\n\n      <h3>Spin Button<\/h3>\n      <p>\n        The button is connected to <code>GPIO33<\/code> and uses the ESP32 internal pull-up. The firmware detects the falling edge from HIGH to LOW and starts a spin if enough credits are available.\n      <\/p>\n\n      <h3>Game State Machine<\/h3>\n      <p>\n        The firmware uses three main states: idle, spinning, and result. During a spin, the reels update with a short animation. After the reels stop, the code checks the winning condition and updates the credits or opens the servo gate.\n      <\/p>\n\n      <h3>Jackpot Gate<\/h3>\n      <p>\n        The servo starts in the closed position. When the slot machine hits 777, the code moves the servo to the open angle, waits for the configured open time, and then closes the gate again.\n      <\/p>\n    <\/div>\n  <\/details>\n\n  <h2>13. Next Improvements<\/h2>\n  <p>\n    The first prototype proves that the concept works. For the next version, the biggest improvements are mechanical and visual rather than electrical.\n  <\/p>\n\n  <div class=\"ebox-card-grid\">\n    <div class=\"ebox-info-card\"><strong>Cleaner housing<\/strong>Improve the 3D model, screw points, display mount, and service access.<\/div>\n    <div class=\"ebox-info-card\"><strong>Better cable management<\/strong>Use connectors, shorter cables, and possibly a small custom PCB.<\/div>\n    <div class=\"ebox-info-card\"><strong>Improved gate<\/strong>Make the locking mechanism stronger, smoother, and harder to bypass.<\/div>\n    <div class=\"ebox-info-card\"><strong>Arcade front panel<\/strong>Add a cleaner button layout, graphics, labels, and a more polished look.<\/div>\n  <\/div>\n\n  <div class=\"ebox-final-box\">\n    <h2>Final Thoughts<\/h2>\n    <p>\n      This project is a fun example of combining simple electronics with a mechanical idea. The electronics are not very complicated on their own: one sensor, one button, one display, and one servo. The interesting part is how these pieces work together to create a small interactive machine.\n    <\/p>\n    <p>\n      Feel free to recreate this project, change the winning chances, modify the display design, or build a completely different housing. If you build your own version, make sure the money chamber is mechanically safe and that the electronics are powered correctly.\n    <\/p>\n  <\/div>\n\n<\/div>\n\n<script>\n  function eboxCopyCode(id, button) {\n    const textarea = document.getElementById(id);\n    if (!textarea) return;\n\n    textarea.focus();\n    textarea.select();\n    textarea.setSelectionRange(0, 999999);\n\n    const oldText = button.textContent;\n\n    if (navigator.clipboard && window.isSecureContext) {\n      navigator.clipboard.writeText(textarea.value).then(function () {\n        button.textContent = 'Copied!';\n        setTimeout(function () {\n          button.textContent = oldText;\n        }, 1400);\n      }).catch(function () {\n        document.execCommand('copy');\n        button.textContent = 'Copied!';\n        setTimeout(function () {\n          button.textContent = oldText;\n        }, 1400);\n      });\n    } else {\n      document.execCommand('copy');\n      button.textContent = 'Copied!';\n      setTimeout(function () {\n        button.textContent = oldText;\n      }, 1400);\n    }\n  }\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>ESP32 Arcade Project Piggy Bank Slot Machine Saving money is usually boring, so I wanted to turn it into a small arcade game. This project combines a piggy bank with a slot machine: every inserted coin gives one spin, small wins give extra spins, and the jackpot unlocks the money chamber. The machine is built [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-856428","page","type-page","status-publish","hentry"],"blocksy_meta":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>piggy-bank-slot-machine<\/title>\n<meta name=\"description\" content=\"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"piggy-bank-slot-machine\" \/>\n<meta property=\"og:description\" content=\"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/\" \/>\n<meta property=\"og:site_name\" content=\"ElektroBox\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-20T17:29:44+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"9 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/\",\"url\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/\",\"name\":\"piggy-bank-slot-machine\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/elektroslot_thumbnail.png\",\"datePublished\":\"2026-06-23T15:11:45+00:00\",\"dateModified\":\"2026-07-20T17:29:44+00:00\",\"description\":\"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/#primaryimage\",\"url\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/elektroslot_thumbnail.png\",\"contentUrl\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/elektroslot_thumbnail.png\",\"width\":1448,\"height\":1086},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/index.php\\\/piggy-bank-slot-machine-esp32\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/elektrobox.ch\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"piggy-bank-slot-machine-esp32\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/#website\",\"url\":\"https:\\\/\\\/elektrobox.ch\\\/\",\"name\":\"ElektroBox\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/#\\\/schema\\\/person\\\/878bd55e17d9d0b1a53523fa5e90f6b2\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/elektrobox.ch\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/#\\\/schema\\\/person\\\/878bd55e17d9d0b1a53523fa5e90f6b2\",\"name\":\"Jacob Jarosz\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Logo_Color-Kopie-scaled.png\",\"url\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Logo_Color-Kopie-scaled.png\",\"contentUrl\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Logo_Color-Kopie-scaled.png\",\"width\":2560,\"height\":2560,\"caption\":\"Jacob Jarosz\"},\"logo\":{\"@id\":\"https:\\\/\\\/elektrobox.ch\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Logo_Color-Kopie-scaled.png\"},\"sameAs\":[\"http:\\\/\\\/elektrobox.ch\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"piggy-bank-slot-machine","description":"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/","og_locale":"en_US","og_type":"article","og_title":"piggy-bank-slot-machine","og_description":"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.","og_url":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/","og_site_name":"ElektroBox","article_modified_time":"2026-07-20T17:29:44+00:00","og_image":[{"url":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png","type":"","width":"","height":""}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"9 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/","url":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/","name":"piggy-bank-slot-machine","isPartOf":{"@id":"https:\/\/elektrobox.ch\/#website"},"primaryImageOfPage":{"@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/#primaryimage"},"image":{"@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/#primaryimage"},"thumbnailUrl":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png","datePublished":"2026-06-23T15:11:45+00:00","dateModified":"2026-07-20T17:29:44+00:00","description":"Build a DIY piggy bank slot machine with an ESP32, coin sensor, spin button, display. Coins give spins, the Jackpot opens the money chamber.","breadcrumb":{"@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/#primaryimage","url":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png","contentUrl":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/06\/elektroslot_thumbnail.png","width":1448,"height":1086},{"@type":"BreadcrumbList","@id":"https:\/\/elektrobox.ch\/index.php\/piggy-bank-slot-machine-esp32\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/elektrobox.ch\/"},{"@type":"ListItem","position":2,"name":"piggy-bank-slot-machine-esp32"}]},{"@type":"WebSite","@id":"https:\/\/elektrobox.ch\/#website","url":"https:\/\/elektrobox.ch\/","name":"ElektroBox","description":"","publisher":{"@id":"https:\/\/elektrobox.ch\/#\/schema\/person\/878bd55e17d9d0b1a53523fa5e90f6b2"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/elektrobox.ch\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/elektrobox.ch\/#\/schema\/person\/878bd55e17d9d0b1a53523fa5e90f6b2","name":"Jacob Jarosz","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/05\/Logo_Color-Kopie-scaled.png","url":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/05\/Logo_Color-Kopie-scaled.png","contentUrl":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/05\/Logo_Color-Kopie-scaled.png","width":2560,"height":2560,"caption":"Jacob Jarosz"},"logo":{"@id":"https:\/\/elektrobox.ch\/wp-content\/uploads\/2026\/05\/Logo_Color-Kopie-scaled.png"},"sameAs":["http:\/\/elektrobox.ch"]}]}},"_links":{"self":[{"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/pages\/856428","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/comments?post=856428"}],"version-history":[{"count":6,"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/pages\/856428\/revisions"}],"predecessor-version":[{"id":856486,"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/pages\/856428\/revisions\/856486"}],"wp:attachment":[{"href":"https:\/\/elektrobox.ch\/index.php\/wp-json\/wp\/v2\/media?parent=856428"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}