// Thay WIFI_SSID, WIFI_PASS, SERVER_BASE trước khi nạp.
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPIFFS.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
const char* WIFI_SSID = "YOUR_SSID"; //thay tên wifi (Tiếng Việt hay có dấu đều được)
const char* WIFI_PASS = "YOUR_PASS"; //thay password wifi
const char* SERVER_BASE = "http://<ip máy tính>:3000"; //thay ip máy tính thành ip máy host server
const char* PLAYER_ENDPOINT = "/player";
const char* LEADERBOARD_ENDPOINT = "/leaderboard.json";
const int btnUpPin = 5;
const int btnDownPin = 2;
const int btnLeftPin = 15;
const int btnRightPin = 4;
const int btnSelectPin = 16;
const int buzzerPin = 18;
#define RIGHT 0
#define UP 1
#define LEFT 2
#define DOWN 3
int score = 0;
int level = 1;
bool isGameOver = false;
struct FOOD { int x; int y; int yes; } food;
struct SNAKE { int x[200]; int y[200]; int node; int dir; } snake;
const char keyMap[][9] = {
"ABCDEFGH",
"IJKLMNOP",
"QRSTUVWX",
"YZ 0123 "
};
const int K_ROWS = 4, K_COLS = 8;
char nameBuf[17]; int nameLen = 0;
char classBuf[17]; int classLen = 0;
unsigned long lastLeaderboardFetch = 0;
const unsigned long FETCH_INTERVAL = 10000; // 10s
volatile int dirFlag = -1;
volatile bool selectFlag = false;
volatile unsigned long lastISRtime = 0;
const unsigned long ISR_DEBOUNCE = 120;
const uint8_t ele[] PROGMEM = {
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
};
void element(int x, int y) { display.drawBitmap(x, y, ele, 8, 8, 1); }
void IRAM_ATTR isrUp(){ if(millis()-lastISRtime>ISR_DEBOUNCE){ dirFlag = UP; lastISRtime = millis(); } }
void IRAM_ATTR isrDown(){ if(millis()-lastISRtime>ISR_DEBOUNCE){ dirFlag = DOWN; lastISRtime = millis(); } }
void IRAM_ATTR isrLeft(){ if(millis()-lastISRtime>ISR_DEBOUNCE){ dirFlag = LEFT; lastISRtime = millis(); } }
void IRAM_ATTR isrRight(){ if(millis()-lastISRtime>ISR_DEBOUNCE){ dirFlag = RIGHT; lastISRtime = millis(); } }
void IRAM_ATTR isrSelect(){ if(millis()-lastISRtime>ISR_DEBOUNCE){ selectFlag = true; lastISRtime = millis(); } }
void setup() {
Serial.begin(115200);
pinMode(btnUpPin, INPUT);
pinMode(btnDownPin, INPUT);
pinMode(btnLeftPin, INPUT);
pinMode(btnRightPin, INPUT);
pinMode(btnSelectPin, INPUT);
pinMode(buzzerPin, OUTPUT);
attachInterrupt(digitalPinToInterrupt(btnUpPin), isrUp, FALLING);
attachInterrupt(digitalPinToInterrupt(btnDownPin), isrDown, FALLING);
attachInterrupt(digitalPinToInterrupt(btnLeftPin), isrLeft, FALLING);
attachInterrupt(digitalPinToInterrupt(btnRightPin), isrRight, FALLING);
attachInterrupt(digitalPinToInterrupt(btnSelectPin), isrSelect, FALLING);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.display();
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS mount failed");
} else Serial.println("SPIFFS mounted");
display.clearDisplay();
display.setTextSize(1); display.setTextColor(WHITE);
display.setCursor(0,0); display.println("WiFi connecting...");
display.display();
WiFi.begin(WIFI_SSID, WIFI_PASS);
int cnt = 0;
while (WiFi.status() != WL_CONNECTED && cnt < 80) {
delay(100);
cnt++;
}
display.clearDisplay();
if (WiFi.status() == WL_CONNECTED) {
display.setCursor(0,0);
display.print("WiFi OK ");
display.println(WiFi.localIP());
Serial.print("WiFi IP: "); Serial.println(WiFi.localIP());
} else {
display.setCursor(0,0);
display.println("WiFi offline");
Serial.println("WiFi not connected");
}
display.display();
delay(600);
}
void virtualKeyboardInput(char *buf, int &len, const char* title) {
int r = 0, c = 0;
bool done = false;
unsigned long lastDraw = 0;
buf[0] = 0; len = 0;
while (!done) {
if (dirFlag != -1) {
int d = dirFlag; dirFlag = -1;
if (d == UP) r = (r -1 + K_ROWS) % K_ROWS;
if (d == DOWN) r = (r +1) % K_ROWS;
if (d == LEFT) c = (c -1 + K_COLS) % K_COLS;
if (d == RIGHT) c = (c +1) % K_COLS;
}
if (selectFlag) {
selectFlag = false;
if (r == 3 && c >= 6) {
if (c == 6) { if (len > 0) { buf[--len] = 0; tone(buzzerPin, 800); delay(80); noTone(buzzerPin); } }
else { done = true; tone(buzzerPin, 1200); delay(120); noTone(buzzerPin); }
} else {
char ch = keyMap[r][c];
if (ch != '\0' && len < 15) { buf[len++] = ch; buf[len] = 0; tone(buzzerPin, 1000); delay(80); noTone(buzzerPin); }
}
}
if (millis() - lastDraw > 80) {
lastDraw = millis();
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0); display.print(title);
display.setCursor(0,8); display.print("Value:"); display.setCursor(50,8); display.print(buf);
int startY = 20;
for (int rr=0; rr<K_ROWS; rr++) {
for (int cc=0; cc<K_COLS; cc++) {
int cellX = cc*15;
int cellY = startY + rr*10;
display.drawRect(cellX, cellY, 14, 9, WHITE);
char ch = (cc < 8) ? keyMap[rr][cc] : ' ';
display.setCursor(cellX+2, cellY+1);
display.print(ch);
}
}
display.setCursor(6*15+2, startY + 3*10 + 1); display.print("<"); // backspace
display.setCursor(7*15+2, startY + 3*10 + 1); display.print("OK"); // done
display.drawRect(c*15 -1, startY + r*10 -1, 16, 11, WHITE);
display.display();
}
delay(20);
}
}
String buildPlayerJson(const char* status) {
String s = "{\"name\":\"";
s += String(nameBuf);
s += "\",\"class\":\"";
s += String(classBuf);
s += "\",\"status\":\"";
s += String(status);
s += "\",\"score\":";
s += String(score);
s += "}";
return s;
}
void saveLocalLeaderboard(const String &json) {
File f = SPIFFS.open("/players.json", "w");
if(!f) { Serial.println("Fail write players.json"); return; }
f.print(json);
f.close();
Serial.println("Saved players.json");
}
String readLocalLeaderboard() {
if (!SPIFFS.exists("/players.json")) return String("[]");
File f = SPIFFS.open("/players.json", "r");
if (!f) return String("[]");
String s; while(f.available()) s += (char)f.read();
f.close();
return s;
}
void sendStatus(const char* status) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi offline, update local players.json");
String local = readLocalLeaderboard();
DynamicJsonDocument doc(16384);
DeserializationError err = deserializeJson(doc, local);
if (err) {
DynamicJsonDocument newDoc(4096);
JsonArray arr = newDoc.to<JsonArray>();
JsonObject o = arr.createNestedObject();
o["name"] = nameBuf; o["class"] = classBuf; o["status"] = status; o["score"] = score; o["updatedAt"] = millis();
String out; serializeJson(arr, out);
saveLocalLeaderboard(out);
return;
}
JsonArray arr = doc.as<JsonArray>();
bool found = false;
for (JsonObject obj : arr) {
if (String((const char*)obj["name"]) == String(nameBuf) && String((const char*)obj["class"]) == String(classBuf)) {
obj["status"] = status; obj["score"] = score; obj["updatedAt"] = millis(); found = true; break;
}
}
if (!found) {
JsonObject o = arr.createNestedObject();
o["name"] = nameBuf; o["class"] = classBuf; o["status"] = status; o["score"] = score; o["updatedAt"] = millis();
}
String out; serializeJson(arr, out);
saveLocalLeaderboard(out);
return;
}
HTTPClient http;
String url = String(SERVER_BASE) + String(PLAYER_ENDPOINT);
http.begin(url);
http.addHeader("Content-Type", "application/json");
String payload = buildPlayerJson(status);
int code = http.POST(payload);
Serial.printf("POST %s -> %d\n", url.c_str(), code);
http.end();
}
void fetchLeaderboardAndSave() {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String url = String(SERVER_BASE) + String(LEADERBOARD_ENDPOINT);
http.begin(url);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
// basic JSON validation
DynamicJsonDocument doc(16384);
if (deserializeJson(doc, payload) == DeserializationError::Ok) {
saveLocalLeaderboard(payload);
Serial.println("Fetched leaderboard saved");
} else Serial.println("Invalid JSON from leaderboard");
} else {
Serial.printf("GET leaderboard code: %d\n", code);
}
http.end();
}
void setupGame(){
randomSeed(analogRead(0));
food = {40,48,1};
snake.x[0]=64; snake.y[0]=32; snake.x[1]=56; snake.y[1]=32; snake.node = 2; snake.dir = RIGHT;
score = 0; level = 1; isGameOver = false;
generateFood();
}
void generateFood() {
do {
food.x = random(0, 16) * 8;
food.y = (random(0, 7) + 1) * 8;
} while (isFoodOnSnake());
}
bool isFoodOnSnake() {
for (int i=0;i<snake.node;i++) if (food.x==snake.x[i] && food.y==snake.y[i]) return true;
return false;
}
void keyUpdate() {
if (dirFlag != -1) {
int d = dirFlag; dirFlag = -1;
if (d == DOWN && snake.dir != UP) snake.dir = DOWN;
if (d == RIGHT && snake.dir != LEFT) snake.dir = RIGHT;
if (d == LEFT && snake.dir != RIGHT) snake.dir = LEFT;
if (d == UP && snake.dir != DOWN) snake.dir = UP;
}
}
void snakeGame() {
switch (snake.dir) {
case RIGHT: snake.x[0] += 8; if (snake.x[0] > 120) snake.x[0]=0; break;
case UP: snake.y[0] -= 8; if (snake.y[0] < 0) snake.y[0]=56; break;
case LEFT: snake.x[0] -= 8; if (snake.x[0] < 0) snake.x[0]=120; break;
case DOWN: snake.y[0] += 8; if (snake.y[0] > 56) snake.y[0]=0; break;
}
if (snake.x[0]==food.x && snake.y[0]==food.y) {
snake.node++; score += 5; level = score/10 + 1;
tone(buzzerPin,1000); delay(80); noTone(buzzerPin);
generateFood();
}
for (int i=1;i<snake.node;i++) if (snake.x[0]==snake.x[i] && snake.y[0]==snake.y[i]) isGameOver = true;
for (int i=snake.node-1;i>0;i--) { snake.x[i]=snake.x[i-1]; snake.y[i]=snake.y[i-1]; }
}
void displaySnake(){ for (int i=0;i<snake.node;i++) element(snake.x[i], snake.y[i]); }
void gameOver() {
tone(buzzerPin,1500); delay(300); noTone(buzzerPin);
display.clearDisplay();
display.setTextSize(2); display.setCursor(10,18); display.println("Game Over");
display.setTextSize(1); display.setCursor(10,42); display.print("Score: "); display.println(score);
display.display();
sendStatus("finished");
fetchLeaderboardAndSave();
delay(1000);
displayLeaderboardFromFile();
delay(3000);
}
void displayLeaderboardFromFile() {
String s = readLocalLeaderboard();
DynamicJsonDocument doc(16384);
DeserializationError err = deserializeJson(doc, s);
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.println("Leaderboard:");
if (err) {
display.setCursor(0,12); display.println("No data");
display.display(); return;
}
JsonArray arr = doc.as<JsonArray>();
int y = 12; int maxShow = 6; int idx = 0;
for (JsonObject obj : arr) {
if (idx >= maxShow) break;
const char* nm = obj["name"] | "";
const char* cl = obj["class"] | "";
int sc = obj["score"] | 0;
display.setCursor(0, y); display.print(idx+1); display.print("."); display.print(nm);
display.setCursor(80, y); display.print(sc);
y += 9; idx++;
}
display.display();
}
void loop() {
virtualKeyboardInput(nameBuf, nameLen, "Enter name:");
virtualKeyboardInput(classBuf, classLen, "Enter class:");
setupGame();
sendStatus("playing");
lastLeaderboardFetch = millis();
while (!isGameOver) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0); display.print("S:"); display.print(score);
display.setCursor(64,0); display.print("Lv:"); display.print(level);
displaySnake();
element(food.x, food.y);
display.display();
keyUpdate();
snakeGame();
if (millis() - lastLeaderboardFetch > FETCH_INTERVAL) {
lastLeaderboardFetch = millis();
fetchLeaderboardAndSave();
}
delay(100);
}
gameOver();
delay(2000);
}