Community project
GameHub Multiplayer Arcade
Adnan khiar aditya rahmat
Published August 4, 2026 · Updated August 11, 2026
Generated with AIGameHub Multiplayer Arcade is a Bluetooth-enabled game console built around an Arduino Uno that hosts multiple party games playable solo or with remote players. The system combines a 16x2 LCD display, three push buttons for navigation, audio feedback via a buzzer, and wireless connectivity through an HC-05 Bluetooth module to create an interactive gaming platform.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for connecting all components safely to the Arduino. You'll also receive the full firmware that powers the game engine, menu system, Bluetooth communication layer, and multiple game modes including spy games, reaction challenges, and a 2048 clone, along with a chat feature for multiplayer coordination.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| LCD 16x2 I2C16x2, I2C address 0x27 | 1 | 16x2 character LCD display with I2C backpack |
| Push ButtonLeft / Up / Next | 1 | Momentary push button switch |
| Push ButtonRight / Down / Select | 1 | Momentary push button switch |
| Push ButtonBack / Reset | 1 | Momentary push button switch |
| Buzzer5 V active buzzer | 1 | Piezo buzzer for sound output |
| LEDRed 5 mm | 1 | Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically. |
| Resistor220 Ω | 1 | Through-hole resistor (current-limiting in series with an LED) |
| Hc 05 Bluetooth Module9600 baud data mode | 1 | Serial Bluetooth module for wireless communication using UART interface. Operates at 9600 baud (Data Mode) or 38400 baud (AT Command Mode). Supports two-way full-duplex wireless functionality with range up to 100m. |
| Resistor2 kΩ | 1 | Through-hole resistor (current-limiting in series with an LED) |
| Resistor1 kΩ | 1 | Through-hole resistor (current-limiting in series with an LED) |
Assembly
6 stepsCreate common power rails
With the Arduino Uno unplugged, connect Uno 5V to the breadboard positive rail and Uno GND to the ground rail. All modules, buttons, LED return, and buzzer negative connection share this ground.
- Tip: Use red wire for 5V and black wire for GND to make checking easier.
- ⚠ Power the finished project from the Uno USB connection only; do not connect a second supply at the same time.
Connect the I2C LCD
Wire LCD VCC to 5V, GND to GND, SDA to Uno A4, and SCL to Uno A5. The firmware uses the common I2C address 0x27.
- Tip: If the backlight is on but text is faint, adjust the LCD contrast trimmer gently.
- ⚠ Do not swap SDA and SCL.
Wire the three buttons
Connect each button between its Uno input and GND: Left/Up to D2, Right/Down to D3, and Enter to D4. The sketch enables the Uno internal pull-up resistors.
- Tip: On a four-pin tactile switch, use pins on opposite sides of the switch.
- Tip: D2 and D3 scroll choices; D4 confirms. On the homepage, a short D4 press sends the shown quick reply, while holding D4 for more than 1.5 seconds opens custom typing.
- ⚠ Never wire these button signal pins to 5V; a press must pull the pin to GND.
Wire the status LED and buzzer
Connect Uno D6 through the 220 ohm resistor to the LED anode (long leg). Connect the LED cathode (short leg/flat side) to GND. Connect buzzer positive/signal to D5 and buzzer negative to GND.
- Tip: The firmware limits the D6 LED PWM output to 50, but the series resistor is still electrically required.
- Tip: The buzzer supplies boot, menu, game, and remote-control sounds.
- ⚠ Never connect the LED directly to D6 without the 220 ohm series resistor.
Wire the HC-05 safely
Connect HC-05 VCC to 5V and GND to the common GND rail. Connect HC-05 TX directly to Uno D10. Connect Uno D11 through the 2 kΩ resistor to HC-05 RX, then connect the 1 kΩ resistor from the HC-05 RX junction to GND.
- Tip: Use a Bluetooth terminal at 9600 baud and configure it to send a newline after a command or message.
- ⚠ Do not connect Uno D11 directly to HC-05 RX; the 2 kΩ / 1 kΩ divider is needed to reduce 5V logic to a safe HC-05 receive level.
Use the homepage, Bluetooth controls, and games
Power the Uno from USB. After the boot animation, the homepage scrolls its welcome message. Use D2/D3 to choose a quick reply and briefly press D4 to send it. Hold D4 for over 1.5 seconds to open custom typing: scroll characters with D2/D3, short-press D4 to add a character, select [SEND] to transmit and return home, or hold D4 to open the main menu. In the main menu, select games, 2048 Micro, or Cara Main. Bluetooth M1–M6 packets remain active: M1 buzzer test, M2 LED flashes, M3 alert, M4 homepage, M5 forced blank in Buckshot, and M6 Imposter sabotage.
- Tip: Incoming ordinary Bluetooth text appears on homepage row 0 briefly.
- Tip: For 2048 Micro, D2 slides left, D3 slides right, and D4 spawns a tile.
- ⚠ Buckshot Roulette is a tabletop game simulation; it does not control or connect to any real weapon.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 5V | lcd_1 VCC | power |
| GND | lcd_1 GND | ground |
| GPIO 18 | lcd_1 SDA | i2c |
| GPIO 19 | lcd_1 SCL | i2c |
| GND | button_left GND | ground |
| GPIO 2 | button_left SIGNAL | digital |
| GND | button_right GND | ground |
| GPIO 3 | button_right SIGNAL | digital |
| GND | button_back GND | ground |
| GPIO 4 | button_back SIGNAL | digital |
| GND | buzzer_1 GND | ground |
| GPIO 5 | buzzer_1 SIGNAL | digital |
| GPIO 6 | led_resistor P1 | digital |
| EXT | led_resistor P2 → LED ANODE | digital |
| GND | led_1 GND | ground |
| 5V | hc05_1 Vcc | power |
| GND | hc05_1 GND | ground |
| GPIO 10 | hc05_1 TX | data |
| GPIO 11 | hc05_rx_top P1 | digital |
| EXT | hc05_rx_top P2 → Hc 05 Bluetooth Module RX | digital |
| EXT | hc05_rx_bottom P1 → Hc 05 Bluetooth Module RX | digital |
| GND | hc05_rx_bottom P2 | ground |
Firmware
Arduino#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
#include <string.h>
#include <stdio.h>
/* GameHub Uno wiring: D2 Up, D3 Down, D4 Enter (all to GND with INPUT_PULLUP);
active buzzer D5; D6 -> 220 ohm resistor -> LED anode; HC-05 TX -> D10 and
D11 -> 2k/1k divider -> HC-05 RX. Do not omit the LED resistor or HC-05 divider. */
enum State:byte { HOMEPAGE, MAIN_MENU, SELECT_PLAYERS, SPY_REVEAL, IMP_REVEAL, SAB_PICK, IMP_PLAY, GUESS, REACT_WAIT, REACT_GO, RESULT, BUCK_LOAD, BUCK_ACTION, BUCK_TARGET, BUCK_INV, BUCK_NOTE, BUCK_OVER, TYPING, CHAT, CHAT_HISTORY, GUIDE_PICK, GUIDE_TEXT, GAME2048, SCORES, SETTINGS, DEVICE_INFO };
enum Game:byte { G_SPY, G_IMP, G_GUESS, G_BUCK };
enum LedFx:byte { FX_NONE, FX_CLICK, FX_START, FX_GUN, FX_ALERT, FX_REMOTE };
// Forward declarations
void readP(char *o,const char *p);
void blinkNormalAlert();
void updateLed();
byte button();
void drawHome();
void showHomepage();
void updateHomepage();
void showMenu();
void startSelected();
void showSpy();
void showImp();
void updateImp();
void updateGuess();
byte nextAlive(byte f);
void fire(bool self,byte target,bool forceBlank);
void showTarget();
void showTyping();
void showChat();
void showChatHistory();
void addChatHistory(const char *text);
void sendChatText(const char *text);
void showGuide();
void spawn2048();
void updateRingtone();
void runPacket();
void updateBluetooth();
void updateDualEnter();
void handle(byte b);
void runBootingScreen();
const byte UP=2, DOWN=3, ENTER=4, BUZZER=5, LED=6, BT_RX=10, BT_TX=11, MAXP=10;
LiquidCrystal_I2C lcd(0x27,16,2); SoftwareSerial bt(BT_RX,BT_TX);
State state=HOMEPAGE; Game pending=G_SPY; LedFx ledFx=FX_NONE;
byte menu=0,totalPlayers=3; unsigned long lastButton=0,ledSince=0,messageSince=0;
// Five fixed 16-character records: no heap allocation on the Uno.
char chatHistory[5][17]; byte chatHistoryCount=0,chatHistoryHead=0,chatHistoryView=0;
bool lcdBacklight=true,buzzerEnabled=true; byte settingsChoice=0;
const char menuNames[][17] PROGMEM={"1. Who The Spy","2. Imposter Game","3. Tebak Kata","4. Buckshot R.","5. 2048 Micro","6. Bluetooth Chat","7. Cara Main","8. High Scores","9. Settings","10. Device Info"};
const char spyA[][10] PROGMEM={"Coffee","Cat","Beach","Pizza","Rain","School"};
const char spyB[][10] PROGMEM={"Tea","Dog","Pool","Burger","Snow","Office"};
const char guessWords[][11] PROGMEM={"ELEPHANT","RAINBOW","ROCKET","GUITAR","PIZZA","VOLCANO"};
const char sabotageNames[][9] PROGMEM={"Lights","Reactor","Comms"};
const char taskNames[][14] PROGMEM={"Wire panels","Scan card","Fuel engine","Empty trash","Align scope","Fix wiring","Calibrate","Start reactor","Clean filter","Upload data"};
const char itemNames[][15] PROGMEM={"Magnify Glass","Cigarette","Handcuffs","Handsaw"};
const char guideNames[][12] PROGMEM={"Who the Spy","Imposter","Tebak Kata","Buckshot","2048 Micro"};
const char quicks[][9] PROGMEM={"Hello!","Yes","No","GG WP","Help!"};
const char alphabet[] PROGMEM=" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?-";
const char welcome[] PROGMEM=" Welcome to GameHub v1.0! ";
// Homepage, Bluetooth, and keyboard buffers are fixed-size to protect Uno SRAM.
byte marquee=0,quickChoice=0; bool promptOn=true,enterHeld=false,enterLong=false;
unsigned long marqueeAt=0,promptAt=0,enterAt=0,btShowUntil=0,remoteAlertUntil=0;
char incoming[17]="",packet[18]="",typed[13]=""; byte packetLen=0,typedLen=0,keyChoice=0;
byte ringtoneStep=0; unsigned long ringtoneAt=0;
// Game state.
byte spy=1,reveal=1,wordPair=0; bool impostor[MAXP]; byte impReveal=1,sabotagePick=0,impAction=0;
bool sabotageActive=false,sabotageFixed=false; unsigned long impStart=0,sabotageAt=0,holdStarted=0,alarmAt=0; byte tasksIssued=0,tasksDone=0;
unsigned long guessStart=0,reactStart=0,reactWait=0; char result0[17]="",result1[17]="";
// Fixed maximum capacity; only indexes below totalPlayers are used.
byte hp[MAXP]; bool alive[MAXP],inventory[MAXP][4],saw[MAXP],skipTurn[MAXP];
byte buckPlayer=0,buckAction=0,buckTarget=0,invChoice=0,aliveCount=0; bool shells[5]; byte shellCount=0,shellPos=0,liveCount=0,blankCount=0;
byte guideChoice=0,guidePage=0; byte tiles[4]={0,0,0,0}; unsigned int score2048=0;
void readP(char *o,const char *p){strcpy_P(o,p);} void line(byte r,const char*s){lcd.setCursor(0,r);byte i=0;while(s[i]&&i<16)lcd.print(s[i++]);while(i++<16)lcd.print(' ');} void screen(const char*a,const char*b){lcd.clear();line(0,a);line(1,b);} void beep(unsigned int f,unsigned int d){if(buzzerEnabled)tone(BUZZER,f,d);} void click(){beep(1050,20);} void triple(){beep(1000,45);delay(60);beep(1000,45);delay(60);beep(1000,45);}
void blinkNormalAlert(){ledFx=FX_ALERT;ledSince=millis();} void blinkMenuScroll(){ledFx=FX_CLICK;ledSince=millis();} void blinkStartTransition(){ledFx=FX_START;ledSince=millis();} void blinkGunshotEffect(){ledFx=FX_GUN;ledSince=millis();} void blinkRemote(){ledFx=FX_REMOTE;ledSince=millis();}
void updateLed(){
unsigned long n=millis(),a=n-ledSince; byte v=0;
if(sabotageActive)v=((n/120UL)&1)?0:50;
else if(ledFx==FX_CLICK){if(a<50)v=50;else ledFx=FX_NONE;}
else if(ledFx==FX_ALERT){if(a<600)v=((a/100UL)&1)?0:50;else ledFx=FX_NONE;}
else if(ledFx==FX_START){if(a<500)v=((a/50UL)&1)?0:50;else ledFx=FX_NONE;}
else if(ledFx==FX_GUN){if(a<800)v=50;else ledFx=FX_NONE;}
else if(ledFx==FX_REMOTE){if(a<1000)v=((a/50UL)&1)?50:0;else ledFx=FX_NONE;}
if(!sabotageActive&&ledFx==FX_NONE){
if(state==HOMEPAGE||state==SELECT_PLAYERS){unsigned int p=n%1600UL;v=(p<800)?p/16:(1600-p)/16;}
else if(state==REACT_GO)v=50;
}
analogWrite(LED,v>50?50:v);
}
// ENTER in chat and typing is resolved on release so short and long presses remain distinct.
byte button(){if(millis()-lastButton<250)return 0;byte b=!digitalRead(UP)?1:(!digitalRead(DOWN)?2:(!digitalRead(ENTER)?3:0));if(b){lastButton=millis();blinkNormalAlert();if(b==3&&(state==HOMEPAGE||state==CHAT||state==TYPING))return 0;}return b;}
void drawHome(){char r[17];byte n=strlen_P(welcome);for(byte i=0;i<16;i++)r[i]=pgm_read_byte(welcome+(marquee+i)%n);r[16]=0;if(millis()<btShowUntil)line(0,incoming);else line(0,r);char q[9];readP(q,quicks[quickChoice]);if(promptOn){char b[17];snprintf(b,sizeof(b),"[%s ENTER]",q);line(1,b);}else line(1,"[ PRESS ENTER ]");}
void showHomepage(){state=HOMEPAGE;sabotageActive=false;marqueeAt=promptAt=millis();marquee=0;promptOn=true;enterHeld=false;lcd.clear();drawHome();}
void updateHomepage(){if(state!=HOMEPAGE)return;unsigned long n=millis();if(n-marqueeAt>=180){marqueeAt=n;marquee=(marquee+1)%strlen_P(welcome);drawHome();}if(n-promptAt>=500){promptAt=n;promptOn=!promptOn;drawHome();}}
void showMenu(){state=MAIN_MENU;sabotageActive=false;char x[17],b[17];readP(x,menuNames[menu]);snprintf(b,sizeof(b),"> %s",x);screen("GameHub Main 1-10",b);} void showPlayers(){char b[17];snprintf(b,sizeof(b),"> %u Players <",totalPlayers);screen("Select Players:",b);} void showResult(const char*a,const char*b){strncpy(result0,a,16);result0[16]=0;strncpy(result1,b,16);result1[16]=0;state=RESULT;screen(result0,result1);}
void startSelected(){
blinkStartTransition();beep(900,40);delay(55);beep(1100,60);
if(pending==G_SPY){state=SPY_REVEAL;spy=random(1,totalPlayers+1);reveal=1;wordPair=random(6);screen("Who the Spy","ENTER: P1");}
else if(pending==G_IMP){memset(impostor,0,sizeof(impostor));byte a=random(totalPlayers),b=random(totalPlayers-1);if(b>=a)b++;impostor[a]=impostor[b]=true;impReveal=1;tasksIssued=tasksDone=0;sabotageActive=false;state=IMP_REVEAL;screen("2 Imposters set","ENTER: P1 role");}
else if(pending==G_GUESS){char w[11],t[17];readP(w,guessWords[random(6)]);guessStart=millis();state=GUESS;snprintf(t,sizeof(t),"Guess: %u players",totalPlayers);screen(t,w);}
else {for(byte p=0;p<totalPlayers;p++){hp[p]=4;alive[p]=true;saw[p]=skipTurn[p]=false;for(byte i=0;i<4;i++)inventory[p][i]=true;}aliveCount=totalPlayers;buckPlayer=0;buckAction=0;shellCount=random(3,6);shellPos=liveCount=blankCount=0;for(byte i=0;i<shellCount;i++){shells[i]=random(2);if(shells[i])liveCount++;else blankCount++;}if(!liveCount){shells[0]=true;liveCount=1;blankCount=shellCount-1;}char c[17];snprintf(c,sizeof(c),"%u Live %u Blank",liveCount,blankCount);state=BUCK_LOAD;messageSince=millis();screen("Magazine loaded",c);}
}
void showSpy(){char w[10],t[17];bool s=reveal==spy;readP(w,s?spyB[wordPair]:spyA[wordPair]);snprintf(t,sizeof(t),"P%u %s",reveal,s?"SPY":"WORD");screen(t,w);} void showImpRole(){char b[17];snprintf(b,sizeof(b),"P%u %s",impReveal,impostor[impReveal-1]?"IMPOSTER":"CREWMATE");screen("Secret role",b);} void showSab(){char n[9],b[17];readP(n,sabotageNames[sabotagePick]);snprintf(b,sizeof(b),"> %s",n);screen("Imposter sabotage",b);} void startImp(){state=IMP_PLAY;impStart=millis();sabotageAt=impStart+30000UL;sabotageActive=false;sabotageFixed=false;impAction=0;screen("Crewmate tasks","Sabotage in 30s");}
void showImp(){char a[17],b[17];snprintf(a,sizeof(a),"Tasks %u/%u",tasksDone,tasksIssued);snprintf(b,sizeof(b),"> %s",impAction?"Fix sabotage":"Do task");screen(a,b);} void activateSab(){if(sabotageActive||sabotageFixed)return;sabotageActive=true;sabotageAt=millis();alarmAt=0;screen("SABOTAGE ACTIVE!","Hold ENTER 3 sec");}
void updateImp(){if(state!=IMP_PLAY)return;unsigned long n=millis();if(!sabotageActive&&!sabotageFixed&&n>=sabotageAt)activateSab();byte due=(n-impStart)/60000UL;if(due>10)due=10;if(tasksIssued<due){char x[14];readP(x,taskNames[tasksIssued++]);screen("NEW TASK!",x);blinkNormalAlert();beep(1250,30);}if(sabotageActive){if(n-alarmAt>=180){alarmAt=n;beep(1900,70);}if(n-sabotageAt>=60000UL){sabotageActive=false;showResult("IMPOSTER WINS!","Sabotage timeout");return;}if(!digitalRead(ENTER)){if(!holdStarted)holdStarted=n;if(n-holdStarted>=3000){sabotageActive=false;sabotageFixed=true;holdStarted=0;screen("SABOTAGE FIXED","Success!");triple();}}else holdStarted=0;}}
void updateGuess(){if(state==GUESS&&millis()-guessStart>=60000UL)showResult("Time is up!","ENTER: menu");} void updateReaction(){if(state==REACT_WAIT&&millis()-reactStart>=reactWait){state=REACT_GO;reactStart=millis();screen("GO! ENTER now","Fast reflex!");beep(1200,50);}}
byte nextAlive(byte f){for(byte n=1;n<=totalPlayers;n++){byte q=(f+n)%totalPlayers;if(alive[q])return q;}return f;} void nextTurn(){saw[buckPlayer]=false;buckPlayer=nextAlive(buckPlayer);if(skipTurn[buckPlayer]){skipTurn[buckPlayer]=false;buckPlayer=nextAlive(buckPlayer);}buckAction=0;} void showBuck(){const char*a[3]={"Shoot Opponent","Shoot Self","Open Inventory"};char t[17],b[17];snprintf(t,sizeof(t),"P%u Turn HP:%u/4",buckPlayer+1,hp[buckPlayer]);snprintf(b,sizeof(b),"> %s",a[buckAction]);screen(t,b);} void buckLoad(){shellCount=random(3,6);shellPos=liveCount=blankCount=0;for(byte i=0;i<shellCount;i++){shells[i]=random(2);if(shells[i])liveCount++;else blankCount++;}if(!liveCount){shells[0]=true;liveCount=1;blankCount=shellCount-1;}char c[17];snprintf(c,sizeof(c),"%u Live %u Blank",liveCount,blankCount);state=BUCK_LOAD;messageSince=millis();screen("Magazine loaded",c);} void winnerCheck(){if(aliveCount!=1)return;for(byte i=0;i<totalPlayers;i++)if(alive[i]){char s[17];snprintf(s,sizeof(s),"Player %u wins!",i+1);state=BUCK_OVER;screen("Buckshot Over",s);}}
void fire(bool self,byte target,bool forceBlank){if(shellPos>=shellCount){buckLoad();return;}bool live=forceBlank?false:shells[shellPos++];byte shooter=buckPlayer;if(live){byte d=saw[shooter]?2:1;hp[target]=(hp[target]>d)?hp[target]-d:0;char b[17];snprintf(b,sizeof(b),"P%u HP:%u",target+1,hp[target]);screen("LIVE SHOT!",b);blinkGunshotEffect();beep(350,150);if(!hp[target]&&alive[target]){alive[target]=false;aliveCount--;winnerCheck();if(state==BUCK_OVER)return;}saw[shooter]=false;nextTurn();state=BUCK_ACTION;showBuck();}else{screen("BLANK shell",self?"Extra turn!":"No damage");beep(900,25);saw[shooter]=false;if(!self)nextTurn();state=BUCK_ACTION;showBuck();}}
void showTarget(){char b[17];snprintf(b,sizeof(b),"Shoot Who? >P%u",buckTarget+1);screen("Choose target",b);} void showInv(){char n[15],b[17];readP(n,itemNames[invChoice]);snprintf(b,sizeof(b),"> %s",inventory[buckPlayer][invChoice]?n:"USED");screen("Inventory",b);} void useItem(){byte p=buckPlayer,i=invChoice;if(!inventory[p][i]){beep(180,50);return;}inventory[p][i]=false;state=BUCK_NOTE;messageSince=millis();if(i==0)screen("Magnify Glass",(shellPos<shellCount&&shells[shellPos])?"Next: LIVE":"Next: BLANK");else if(i==1){if(hp[p]<4)hp[p]++;screen("Cigarette +1 HP","Max health 4");}else if(i==2){skipTurn[nextAlive(p)]=true;screen("Handcuffs used","Next turn skipped");}else{saw[p]=true;screen("Handsaw armed","Next live = 2HP");}beep(900,30);} void updateBuck(){if(state==BUCK_LOAD&&millis()-messageSince>=1300){state=BUCK_ACTION;showBuck();}if(state==BUCK_NOTE&&millis()-messageSince>=2000){state=BUCK_ACTION;showBuck();}}
void addChatHistory(const char *text){if(!text[0])return;strncpy(chatHistory[chatHistoryHead],text,16);chatHistory[chatHistoryHead][16]=0;chatHistoryHead=(chatHistoryHead+1)%5;if(chatHistoryCount<5)chatHistoryCount++;}
void sendChatText(const char *text){if(!text[0])return;bt.println(text);addChatHistory(text);beep(1100,35);blinkMenuScroll();}
void showChat(){state=CHAT;char q[9],row[17];if(quickChoice<5){readP(q,quicks[quickChoice]);snprintf(row,sizeof(row),"> %s",q);}else if(quickChoice==5)strcpy(row,"> [View History]");else strcpy(row,"> [ Back ]");screen(incoming[0]?incoming:"BT Chat ready",row);}
void showChatHistory(){state=CHAT_HISTORY;if(!chatHistoryCount){screen("Chat Bank","No messages");return;}chatHistoryView=0;byte slot=(chatHistoryHead+4)%5;screen("History newest",chatHistory[slot]);}
void showTyping(){state=TYPING;char top[17],bot[17];const byte nc=strlen_P(alphabet);if(keyChoice<nc){char c=pgm_read_byte(alphabet+keyChoice);snprintf(top,sizeof(top),"%s",typed);snprintf(bot,sizeof(bot),"< %c > ENTER",c);}else if(keyChoice==nc){snprintf(top,sizeof(top),"%s",typed);strcpy(bot,"[BACKSPACE]");}else if(keyChoice==nc+1){snprintf(top,sizeof(top),"%s",typed);strcpy(bot,"[SEND]");}else{snprintf(top,sizeof(top),"%s",typed);strcpy(bot,"[EXIT TO MENU]");}screen(top,bot);}
void showGuide(){state=GUIDE_PICK;char n[12],b[17];readP(n,guideNames[guideChoice]);snprintf(b,sizeof(b),"> %s",n);screen("Cara Main",b);} void showGuideText(){char a[17],b[17];if(guideChoice==0){strcpy(a,guidePage?"Win: find Spy":"Spy gets close word");strcpy(b,guidePage?"Buttons reveal":"ENTER shows roles");}else if(guideChoice==1){strcpy(a,guidePage?"Fix: hold ENTER":"2 Imposters; tasks");strcpy(b,guidePage?"for 3 seconds":"Beat sabotage timer");}else if(guideChoice==2){strcpy(a,guidePage?"Finish before 60s":"One player guesses");strcpy(b,guidePage?"ENTER completes":"Others give clues");}else if(guideChoice==3){strcpy(a,guidePage?"Last HP wins":"Choose shoot/item");strcpy(b,guidePage?"Live hurts; blank":"UP/DOWN then ENTER");}else{strcpy(a,guidePage?"Merge equal tiles":"UP=left DOWN=right");strcpy(b,guidePage?"Get highest score":"ENTER spawns tile");}screen(a,b);}
void spawn2048(){byte open[4],n=0;for(byte i=0;i<4;i++)if(!tiles[i])open[n++]=i;if(n)tiles[open[random(n)]]=random(10)?1:2;} void show2048(){char a[17],b[17];char *p=a;for(byte i=0;i<4;i++){if(tiles[i])p+=sprintf(p,"%u ",1<<tiles[i]);else p+=sprintf(p,". ");}snprintf(b,sizeof(b),"Score:%u ENTER+",score2048);screen(a,b);} void slide2048(bool left){byte x[4]={0,0,0,0},n=0;for(byte z=0;z<4;z++){byte i=left?z:3-z;if(tiles[i])x[n++]=tiles[i];}for(byte i=0;i+1<n;i++)if(x[i]==x[i+1]){x[i]++;score2048+=(1<<x[i]);for(byte j=i+1;j+1<n;j++)x[j]=x[j+1];n--;i++;}memset(tiles,0,sizeof(tiles));for(byte z=0;z<n;z++)tiles[left?z:3-z]=x[z];spawn2048();show2048();}
void updateRingtone(){if(!ringtoneStep)return;if(millis()-ringtoneAt<110)return;const unsigned int notes[4]={1200,1600,1200,1900};if(ringtoneStep<4){beep(notes[ringtoneStep++],75);ringtoneAt=millis();}else ringtoneStep=0;} void remoteAlert(){remoteAlertUntil=millis()+2000UL;screen("REMOTE ALERT!","M3 received");}
void runPacket(){if(!strcmp(packet,"M1")){ringtoneStep=1;ringtoneAt=millis();beep(1200,75);}else if(!strcmp(packet,"M2"))blinkRemote();else if(!strcmp(packet,"M3"))remoteAlert();else if(!strcmp(packet,"M4"))showHomepage();else if(!strcmp(packet,"M5")){if(state==BUCK_ACTION)fire(true,buckPlayer,true);}else if(!strcmp(packet,"M6")){if(state==IMP_PLAY)activateSab();}else{strncpy(incoming,packet,16);incoming[16]=0;addChatHistory(incoming);btShowUntil=millis()+2000UL;if(state==HOMEPAGE)drawHome();else if(state==CHAT)showChat();}}
void updateBluetooth(){while(bt.available()){char c=(char)bt.read();if(c=='\r'||c=='\n'){if(packetLen){packet[packetLen]=0;runPacket();packetLen=0;}}else if(packetLen<sizeof(packet)-1)packet[packetLen++]=c;}}
// Enter is resolved on release: short sends/adds; a 1.5-second hold changes to its dedicated screen.
void updateDualEnter(){if(state!=CHAT&&state!=TYPING)return;bool down=!digitalRead(ENTER);unsigned long n=millis();if(down&&!enterHeld){enterHeld=true;enterAt=n;enterLong=false;}if(down&&enterHeld&&!enterLong&&n-enterAt>1500UL){enterLong=true;if(state==CHAT)showTyping();else showMenu();}if(!down&&enterHeld){bool wasLong=enterLong;enterHeld=false;if(wasLong)return;if(state==CHAT){if(quickChoice<5){char q[9];readP(q,quicks[quickChoice]);sendChatText(q);showChat();}else if(quickChoice==5)showChatHistory();else showMenu();}else if(state==TYPING){byte nc=strlen_P(alphabet);if(keyChoice<nc){if(typedLen<12){typed[typedLen++]=pgm_read_byte(alphabet+keyChoice);typed[typedLen]=0;}}else if(keyChoice==nc){if(typedLen)typed[--typedLen]=0;}else if(keyChoice==nc+1){sendChatText(typed);typedLen=0;typed[0]=0;showChat();return;}else{showMenu();return;}showTyping();}}}
void handle(byte b){
if(!b)return;
if(state==HOMEPAGE){if(b==3)showMenu();return;}
if(state==MAIN_MENU){
if(b<3){menu=(b==1)?(menu+9)%10:(menu+1)%10;click();blinkMenuScroll();showMenu();return;}
if(menu<=3){pending=(Game)menu;totalPlayers=3;state=SELECT_PLAYERS;showPlayers();}
else if(menu==4){memset(tiles,0,sizeof(tiles));score2048=0;spawn2048();spawn2048();state=GAME2048;show2048();}
else if(menu==5){quickChoice=0;showChat();}
else if(menu==6){guideChoice=guidePage=0;showGuide();}
else if(menu==7){state=SCORES;char r[17];snprintf(r,sizeof(r),"2048: %u",score2048);screen("High Scores",r);}
else if(menu==8){state=SETTINGS;settingsChoice=0;screen("Settings","> LCD Backlight");}
else {state=DEVICE_INFO;screen("GameHub OS v1.2","Free RAM: OK");}
return;
}
if(state==SELECT_PLAYERS){if(b==1&&totalPlayers<10){totalPlayers++;click();blinkMenuScroll();showPlayers();}else if(b==2&&totalPlayers>3){totalPlayers--;click();blinkMenuScroll();showPlayers();}else if(b==3)startSelected();return;}
if(state==SPY_REVEAL){if(b==3)showSpy();else if(reveal<totalPlayers){reveal++;screen("Who the Spy","ENTER: reveal");}else showMenu();return;}
if(state==IMP_REVEAL){if(b==3)showImpRole();else if(impReveal<totalPlayers){impReveal++;screen("Imposter Game","ENTER: role");}else{state=SAB_PICK;sabotagePick=0;showSab();}return;}
if(state==SAB_PICK){if(b==3)startImp();else{sabotagePick=(sabotagePick+1)%3;showSab();}return;}
if(state==IMP_PLAY){if(b<3){impAction=1-impAction;showImp();}else if(!impAction&&tasksDone<tasksIssued){tasksDone++;screen("Task complete!","Crewmate work");beep(900,35);}return;}
if(state==GUESS){if(b==3)showResult("Guess complete","ENTER: menu");return;}
if(state==REACT_WAIT&&b==3){showResult("Too early!","ENTER: menu");return;}
if(state==REACT_GO&&b==3){char x[17];snprintf(x,sizeof(x),"%lu ms",millis()-reactStart);showResult("Your time:",x);return;}
if((state==RESULT||state==BUCK_OVER||state==SCORES||state==DEVICE_INFO)&&b==3){showMenu();return;}
if(state==BUCK_ACTION){if(b<3){buckAction=(b==1)?(buckAction+2)%3:(buckAction+1)%3;click();blinkMenuScroll();showBuck();}else if(buckAction==0){buckTarget=nextAlive(buckPlayer);state=BUCK_TARGET;showTarget();}else if(buckAction==1)fire(true,buckPlayer,false);else{state=BUCK_INV;invChoice=0;showInv();}return;}
if(state==BUCK_TARGET){if(b<3){buckTarget=nextAlive(buckTarget);if(buckTarget==buckPlayer)buckTarget=nextAlive(buckTarget);click();blinkMenuScroll();showTarget();}else fire(false,buckTarget,false);return;}
if(state==BUCK_INV){if(b<3){invChoice=(b==1)?(invChoice+3)%4:(invChoice+1)%4;click();blinkMenuScroll();showInv();}else useItem();return;}
if(state==CHAT){if(b<3){quickChoice=(b==1)?(quickChoice+6)%7:(quickChoice+1)%7;click();blinkMenuScroll();showChat();}return;}
if(state==CHAT_HISTORY){if(b<3&&chatHistoryCount){if(b==1&&chatHistoryView+1<chatHistoryCount)chatHistoryView++;if(b==2&&chatHistoryView)chatHistoryView--;byte slot=(chatHistoryHead+5-1-chatHistoryView)%5;screen("Chat Bank",chatHistory[slot]);}else if(b==3)showChat();return;}
if(state==TYPING){byte nc=strlen_P(alphabet);if(b<3){byte count=nc+3;keyChoice=(b==1)?(keyChoice+count-1)%count:(keyChoice+1)%count;click();blinkMenuScroll();showTyping();}return;}
if(state==GUIDE_PICK){if(b<3){guideChoice=(b==1)?(guideChoice+4)%5:(guideChoice+1)%5;showGuide();}else{state=GUIDE_TEXT;guidePage=0;showGuideText();}return;}
if(state==GUIDE_TEXT){if(b<3){guidePage=1-guidePage;showGuideText();}else showGuide();return;}
if(state==GAME2048){if(b==1)slide2048(true);else if(b==2)slide2048(false);else{spawn2048();show2048();}return;}
if(state==SETTINGS){if(b<3){settingsChoice=1-settingsChoice;screen("Settings",settingsChoice?"> Buzzer Sound":"> LCD Backlight");}else if(!settingsChoice){lcdBacklight=!lcdBacklight;if(lcdBacklight)lcd.backlight();else lcd.noBacklight();}else buzzerEnabled=!buzzerEnabled;return;}
}
void runBootingScreen(){lcd.clear();line(0," GAMEHUB v1.0 ");beep(440,100);delay(110);beep(554,100);delay(110);beep(659,200);delay(210);for(byte f=0;f<=14;f++){char bar[17];bar[0]='[';for(byte i=0;i<14;i++)bar[i+1]=(i<f)?'>':' ';bar[15]=']';bar[16]=0;line(1,bar);analogWrite(LED,50);tone(BUZZER,1200,15);delay(75);analogWrite(LED,0);delay(75);}beep(880,250);delay(260);lcd.clear();}
void setup(){pinMode(UP,INPUT_PULLUP);pinMode(DOWN,INPUT_PULLUP);pinMode(ENTER,INPUT_PULLUP);pinMode(BUZZER,OUTPUT);pinMode(LED,OUTPUT);lcd.init();lcd.backlight();bt.begin(9600);randomSeed(analogRead(A0)^micros());runBootingScreen();showHomepage();}
void loop(){updateBluetooth();updateDualEnter();handle(button());updateHomepage();updateImp();updateGuess();updateReaction();updateBuck();updateRingtone();updateLed();}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.