Community project

GameHub Multiplayer Arcade

Arduino
Photo of GameHub Multiplayer Arcade
Generated with AI

Adnan khiar aditya rahmat

Last updated August 11, 2026

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

Wiring diagram for GameHub Multiplayer Arcade

Gather all the parts

QtyComponent
1

LCD 16x2 I2C

16x2, I2C address 0x27

16x2 character LCD display with I2C backpack

1

Push Button

Left / Up / Next

Momentary push button switch

1

Push Button

Right / Down / Select

Momentary push button switch

1

Push Button

Back / Reset

Momentary push button switch

1

Buzzer

5 V active buzzer

Piezo buzzer for sound output

1

LED

Red 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Hc 05 Bluetooth Module

9600 baud data mode

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.

1

Resistor

2 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

1 kΩ

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 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.

  • 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.

  • 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.

  • On a four-pin tactile switch, use pins on opposite sides of the switch.
  • 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.

  • The firmware limits the D6 LED PWM output to 50, but the series resistor is still electrically required.
  • 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.

  • 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.

  • Incoming ordinary Bluetooth text appears on homepage row 0 briefly.
  • 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.

Review all connections

1. Connections between "lcd_1" and "Arduino"

Functionlcd_1Arduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

2. Connections between "button_left" and "Arduino"

Functionbutton_leftArduino
groundGNDGND
digitalSIGNALGPIO 2

3. Connections between "button_right" and "Arduino"

Functionbutton_rightArduino
groundGNDGND
digitalSIGNALGPIO 3

4. Connections between "button_back" and "Arduino"

Functionbutton_backArduino
groundGNDGND
digitalSIGNALGPIO 4

5. Connections between "buzzer_1" and "Arduino"

Functionbuzzer_1Arduino
groundGNDGND
digitalSIGNALGPIO 5

6. Connections between "led_resistor" and "Arduino"

Functionled_resistorArduino
digitalP1GPIO 6
digitalP2LED ANODEEXT

7. Connections between "led_1" and "Arduino"

Functionled_1Arduino
groundGNDGND

8. Connections between "hc05_1" and "Arduino"

Functionhc05_1Arduino
powerVcc5V
groundGNDGND
dataTXGPIO 10

9. Connections between "hc05_rx_top" and "Arduino"

Functionhc05_rx_topArduino
digitalP1GPIO 11
digitalP2Hc 05 Bluetooth Module RXEXT

10. Connections between "hc05_rx_bottom" and "Arduino"

Functionhc05_rx_bottomArduino
digitalP1Hc 05 Bluetooth Module RXEXT
groundP2GND

Deploy the firmware

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

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