/*********************************************************************
This project is inspired and based on ili9341-hello.ino, which is a
open-source project of WOKWI. Instead of an Arduino, a ESP32 is used
as the uc, while the pins-configuration are also slightly adapted to
it. Parts of the software is modified, which includes:
- Replace Hexcode-Color with colors for Adafruit library
- Redesign the homescreen of the game
- Delete some magic numbers
- Add functions
- New game rule: Snake is allowed to cross its own body
- Add MQTT to upload Highscore on UBIDOTS
*********************************************************************/
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <WiFi.h>
#include <PubSubClient.h>
#define WIFISSID "Wokwi-GUEST" // Put your WifiSSID here
#define PASSWORD "" // Put your wifi password here
#define TOKEN "BBUS-fj0Fv4fU07P2ZwFCqfPlOyAQBOKO1y" // Put your Ubidots' TOKEN
#define MQTT_CLIENT_NAME "SNAKE" // MQTT client Name, please enter your own 8-12 alphanumeric character ASCII string;
//Setup for MQTT
#define VARIABLE_LABEL "score" // Assing the variable label
#define VARIABLE_LABEL_SUBSCRIBE "snake" // Assing the variable label
#define DEVICE_LABEL "snake" // Assig the device label
char mqttBroker[] = "industrial.api.ubidots.com";
char payload[100];
char topic[150];
char topicSubscribe[100];
WiFiClient ubidots;
PubSubClient client(ubidots);
//Setup for the ILI9341 (MISO & MOSI has be connected to certain pins according to the WOKWI Reference)
#define TFT_DC 17
#define TFT_CS 5
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
//Setup for the joystick
#define JOY_VERT 35
#define JOY_HORIZ 34
#define JOY_SEL 32
//Setup for the screen size
#define SCREEN_WIDTH 14
#define SCREEN_HEIGHT 19
//Define the directions
#define NORTH 2
#define EAST 1
#define SOUTH -2
#define WEST -1
#define CENTER 0
struct SnakeBody{
unsigned int x;
unsigned int y;
unsigned int XMax;
unsigned int XMin;
unsigned int YMax;
unsigned int YMin;
bool isSpecial;
};
SnakeBody snake[100];
SnakeBody food[1];
unsigned int size = 3;
unsigned int score = 0;
unsigned int highScore = 0;
int direction = EAST;
int speed = 300;
//Create HomeScreen
void home_screen(){
tft.fillScreen(ILI9341_LIGHTGREY);
home_screen_snake();
home_screen_starttext();
htl_logo();
}
void home_screen_snake(){
tft.setCursor(50, 100);
tft.setTextColor(ILI9341_BLACK);
tft.setTextSize(5);
tft.println("SNAKE");
tft.setCursor(70, 160);
tft.println("GAME");
}
void home_screen_starttext(){
tft.setCursor(20, 240);
tft.setTextColor(ILI9341_BLACK);
tft.setTextSize(2);
tft.println("> PRESS TO PLAY <");
}
void htl_logo(){
tft.fillRect(10,10,10,10,ILI9341_YELLOW);
tft.fillRect(22,10,10,10,ILI9341_YELLOW);
tft.fillRect(22,22,10,10,ILI9341_ORANGE);
tft.fillRect(10,34,10,10,ILI9341_RED);
tft.fillRect(34,34,10,10,ILI9341_MAGENTA);
tft.fillRect(46,22,10,10,ILI9341_GREEN);
tft.fillRect(58,34,10,10,ILI9341_DARKGREEN);
tft.fillRect(46,46,10,10,ILI9341_NAVY);
tft.fillRect(22,46,10,10,ILI9341_PURPLE);
}
void play(){
// Initializng the position of the food
food[0].x = 10;
food[0].y = 10;
food[0].XMax = 16 * food[0].x + 16;
food[0].XMin = 16 * food[0].x;
food[0].YMax = 16 * food[0].y + 16;
food[0].YMin = 16 * food[0].y;
// Initializing the starting postion of the snake
snake[0].x = 6;
snake[0].y = 10;
snake[1].x = 6;
snake[1].y = 10;
// Draw boarders of the display
for (int i = 0; i < size; i++){
snake[i].XMax = 16 * snake[i].x + 16;
snake[i].XMin = 16 * snake[i].x;
snake[i].YMax = 16 * snake[i].y + 16;
snake[i].YMin = 16 * snake[i].y;
}
direction = EAST;
size = 3;
bool game_state = true;
while (game_state == true){
// Prevents the snake from going in the exact opposite direction it is traveling in
if (direction != -joystick() && joystick() != CENTER){
// Changes direction to the direction of the joystick
direction = joystick();
}
// Moves snake head in the direction of travel
snake_movement(direction);
// Moves rest of snake body accordingly (func pear is inside that checks for food collision)
update_snake();
game_state = check_collision();
}
return;
}
void snake_movement(int direction){
if (direction == NORTH){
snake[0].y++;
}
else if (direction == SOUTH){
snake[0].y--;
}
else if (direction == WEST){
snake[0].x--;
}
else if (direction == EAST){
snake[0].x++;
}
}
// Reads the joystick
int joystick(){
if(abs(analogRead(JOY_VERT) - 2048) > 2){
if(analogRead(JOY_VERT) < 2040){
return NORTH;
}
else if(analogRead(JOY_VERT) > 2056){
return SOUTH;
}
}
else if(abs(analogRead(JOY_HORIZ) - 2048) > 2){
if(analogRead(JOY_HORIZ) < 2040){
return EAST;
}
else if(analogRead(JOY_HORIZ) > 2056){
return WEST;
}
}
else{
return CENTER;
}
}
// Checks for collision with food
void food_collision() {
if (food[0].x == snake[0].x && food[0].y == snake[0].y) {
if (food[0].isSpecial) {
size += 2; // Increase size by 2 for special food
} else {
size++;
}
gen_food();
food[0].XMax = 16 * food[0].x + 16;
food[0].XMin = 16 * food[0].x;
food[0].YMax = 16 * food[0].y + 16;
food[0].YMin = 16 * food[0].y;
if (speed > 200) {
speed = speed - 10;
}
}
tft.fillRect(food[0].XMin, food[0].YMin, 15, 15, food[0].isSpecial ? ILI9341_ORANGE : ILI9341_RED);
}
// Generates valid random positions for the food to spawn
void gen_food() {
food[0].x = random(1, SCREEN_WIDTH);
food[0].y = random(3, SCREEN_HEIGHT);
food[0].isSpecial = random(0, 100) < 50; // 50% chance of being special food
for (int i = 0; i < size; i++) {
if (food[0].x == snake[i].x && food[0].y == snake[i].y) {
gen_food();
break;
}
}
}
// Moves the snake
void update_snake() {
for (int i = 0; i < size; i++) {
tft.fillRect(snake[i].XMin, snake[i].YMin, 15, 15, ILI9341_LIGHTGREY);
}
for (int i = size; i > 0; i--) {
snake[i].x = snake[i-1].x;
snake[i].y = snake[i-1].y;
}
for (int i = 0; i < size; i++) {
snake[i].XMax = 16 * snake[i].x + 16;
snake[i].XMin = 16 * snake[i].x;
snake[i].YMax = 16 * snake[i].y + 16;
snake[i].YMin = 16 * snake[i].y;
}
for (int i = 1; i < size; i++) {
tft.fillRect(snake[i].XMin, snake[i].YMin, 15, 15, ILI9341_BLACK);
}
tft.fillRect(snake[0].XMin, snake[0].YMin, 15, 15, ILI9341_BLACK);
food_collision();
delay(speed);
}
bool check_collision(){
// Check if the head leaves the screen
if (snake[0].x > SCREEN_WIDTH-1 || snake[0].x < 1){
lose();
return false;
}
if (snake[0].y > SCREEN_HEIGHT-1 || snake[0].y < 1){
lose();
return false;
}
return true;
}
void lose(){
char str_score[10];
// Save the score of the game
score = size - 3;
if (score > highScore){
highScore = score;
}
// Clear the game - alse end the game
clear();
// Print the game over messages to the screen
tft.setTextColor(ILI9341_RED);
tft.setCursor(15, 75);
tft.setTextSize(4);
tft.println("GAME OVER");
// Prints the score of the game to the screen
tft.setTextColor(ILI9341_BLACK);
tft.setCursor(40, 135);
tft.setTextSize(3);
tft.println("SCORE: ");
tft.setCursor(150, 135);
tft.println(score);
tft.setTextSize(2);
tft.setCursor(30, 175);
tft.println("HIGH SCORE: ");
tft.setCursor(175, 175);
tft.println(highScore);
// Upload highscore to Ubidots
dtostrf(highScore, 0, 0, str_score);
sprintf(payload, "%s {\"value\": %s}}", payload, str_score);
client.publish(topic, payload);
tft.setTextColor(ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(35, 220);
tft.println("PRESS JOYSTICK");
tft.setCursor(40, 240);
tft.println("TO PLAY AGAIN");
// Resets the score of the game
score = 0;
client.loop();
}
void clear(){
// tft.fillScreen(ILI9341_LIGHTGREY);
for (int i = 0; i < size; i++){
tft.fillRect(snake[i].XMin, snake[i].YMin, 16, 16, ILI9341_LIGHTGREY);
}
tft.fillRect(food[0].XMin, food[0].YMin, 16, 16, ILI9341_LIGHTGREY);
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.println("Attempting MQTT connection...");
// Attemp to connect
if (client.connect(MQTT_CLIENT_NAME, TOKEN, "")) {
Serial.println("Connected");
client.subscribe(topicSubscribe);
} else {
Serial.print("Failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 2 seconds");
// Wait 2 seconds before retrying
delay(2000);
}
}
}
void callback(char* topic, byte* payload, unsigned int length) {
char p[length + 1];
memcpy(p, payload, length);
p[length] = NULL;
String message(p);
int menu=message.toInt();
}
void setup(){
Serial.begin(115200);
Serial.println("Initializing TFT...");
tft.begin();
Serial.println("Initializing random seed...");
randomSeed(analogRead(A4));
home_screen();
Serial.println("Setting up joystick...");
pinMode(JOY_SEL, INPUT_PULLUP);
WiFi.begin(WIFISSID, PASSWORD);
Serial.println("Connecting to WiFi...");
Serial.println();
Serial.print("Wait for WiFi...");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println("");
Serial.println("WiFi Connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
client.setServer(mqttBroker, 1883);
// Additional debugging information
Serial.println("Setup complete");
client.setCallback(callback);
sprintf(topicSubscribe, "/v1.6/devices/%s/%s/lv", DEVICE_LABEL, VARIABLE_LABEL_SUBSCRIBE);
client.subscribe(topicSubscribe);
}
void loop(){
if (!client.connected()) {
client.subscribe(topicSubscribe);
reconnect();
}
sprintf(topic, "%s%s", "/v1.6/devices/", DEVICE_LABEL);
sprintf(payload, "%s", ""); // Cleans the payload
sprintf(payload, "\"%s\":", VARIABLE_LABEL); // Adds the variable label
if (digitalRead(JOY_SEL) == LOW){
tft.fillScreen(ILI9341_LIGHTGREY);
tft.drawRect(9, 9, 222, 300, ILI9341_BLACK); //Rahmen
tft.setCursor(50, 5);
tft.setTextColor(ILI9341_BLACK);
tft.setTextSize(3);
play();
}
}