Community project

GameHub Multiplayer Arcade

Adnan khiar aditya rahmat

Published August 4, 2026 · Updated August 11, 2026

Arduino10 components6 assembly steps
Remix this project
Photo of GameHub Multiplayer ArcadeGenerated with AI

GameHub 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
Wiring diagram for GameHub Multiplayer Arcade

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
LCD 16x2 I2C16x2, I2C address 0x27116x2 character LCD display with I2C backpack
Push ButtonLeft / Up / Next1Momentary push button switch
Push ButtonRight / Down / Select1Momentary push button switch
Push ButtonBack / Reset1Momentary push button switch
Buzzer5 V active buzzer1Piezo buzzer for sound output
LEDRed 5 mm1Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.
Resistor220 Ω1Through-hole resistor (current-limiting in series with an LED)
Hc 05 Bluetooth Module9600 baud data mode1Serial 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Ω1Through-hole resistor (current-limiting in series with an LED)
Resistor1 kΩ1Through-hole resistor (current-limiting in series with an LED)

Assembly

6 steps
  1. Create 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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
PinConnectionType
5Vlcd_1 VCCpower
GNDlcd_1 GNDground
GPIO 18lcd_1 SDAi2c
GPIO 19lcd_1 SCLi2c
GNDbutton_left GNDground
GPIO 2button_left SIGNALdigital
GNDbutton_right GNDground
GPIO 3button_right SIGNALdigital
GNDbutton_back GNDground
GPIO 4button_back SIGNALdigital
GNDbuzzer_1 GNDground
GPIO 5buzzer_1 SIGNALdigital
GPIO 6led_resistor P1digital
EXTled_resistor P2LED ANODEdigital
GNDled_1 GNDground
5Vhc05_1 Vccpower
GNDhc05_1 GNDground
GPIO 10hc05_1 TXdata
GPIO 11hc05_rx_top P1digital
EXThc05_rx_top P2Hc 05 Bluetooth Module RXdigital
EXThc05_rx_bottom P1Hc 05 Bluetooth Module RXdigital
GNDhc05_rx_bottom P2ground

Firmware

Arduino
main.cppDeploy to device
#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.

Open in Schematik