Compare commits

..

9 Commits

12 changed files with 314 additions and 84 deletions
+15
View File
@@ -0,0 +1,15 @@
#pragma once
/*
* A header file to store all the constants used in the project.
*/
#include <Arduino.h>
#define LCD_I2C_ADDRESS 0x27
#define LCD_COLS 20
#define LCD_ROWS 4
#define ACTION_INTERVAL 60000 // 1 minute
#define MAXIMUM_STAT 100
#define ANIMATION_FRAME_INTERVAL 1500 // Time in milliseconds between animation frames
#define DEBOUNCE_DELAY 50 // Debounce delay in milliseconds for the joystick button
+5 -3
View File
@@ -1,23 +1,25 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <LiquidCrystal_I2C.h> #include <LiquidCrystal_I2C.h>
#include "constants.hpp"
#include "menu.hpp" #include "menu.hpp"
#include "pet.hpp"
/* /*
* A helper class to facilitate drawing on a HD44780 LCD display. * A helper class to facilitate drawing on a HD44780 LCD display.
*/ */
class Display { class Display {
public: public:
Display(uint8_t addr = 0x27, uint8_t cols = 20, uint8_t rows = 4); Display();
void begin(); void begin();
void clear(); void clear();
void drawPetStats(Pet& pet);
void drawPet(Pet& pet);
void drawBuffer(String buffer[]); void drawBuffer(String buffer[]);
void drawMenu(Menu& menu); void drawMenu(Menu& menu);
LiquidCrystal_I2C& getLCD(); LiquidCrystal_I2C& getLCD();
private: private:
uint8_t rows;
uint8_t cols;
LiquidCrystal_I2C lcd; LiquidCrystal_I2C lcd;
}; };
+5 -11
View File
@@ -1,22 +1,16 @@
#pragma once #pragma once
#include <LibPrintf.h>
#include "joystick.hpp" #include "joystick.hpp"
#include "display.hpp" #include "display.hpp"
#include "menu.hpp" #include "menu.hpp"
#define ACTION_INTERVAL 60000 // 1 minute #include "pet.hpp"
#define MAXIMUM_STAT 100
typedef struct { typedef struct {
bool dead; Pet pet;
bool menuOpen;
int hunger;
int joy;
int energy;
int cleanliness;
uint64_t lastActionTime; uint64_t lastActionTime;
bool shouldClear;
bool isMenuOpen;
bool shouldClearDisplay;
} GameState; } GameState;
class Game { class Game {
+17 -4
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include "constants.hpp"
/* /*
* A helper class to read values from the KY-023 joystick module. * A helper class to read values from the KY-023 joystick module.
@@ -25,15 +26,27 @@ class Joystick {
Joystick(); Joystick();
double getX() const; double getX() const;
double getY() const; double getY() const;
bool isPressed() const; bool isPressed();
JoystickDirection getDirection() const; JoystickDirection getDirection() const;
private: private:
class Switch {
public:
Switch(uint8_t pin);
bool isPressed();
private:
uint8_t pin;
bool lastKnownState;
bool lastStableState;
uint64_t lastDebounceTime;
};
uint8_t vrx; uint8_t vrx;
uint8_t vry; uint8_t vry;
uint8_t sw; Switch sw;
uint8_t xOffset;
uint8_t yOffset;
const uint8_t xOffset = 13;
const uint8_t yOffset = 10;
const uint8_t deadzone = 15; // Deadzone threshold to prevent jitter around the center position const uint8_t deadzone = 15; // Deadzone threshold to prevent jitter around the center position
}; };
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#define MENU_ITEM_COUNT 4
#include <Arduino.h> #include <Arduino.h>
#include "constants.hpp"
#include "joystick.hpp" #include "joystick.hpp"
class Menu { class Menu {
@@ -12,6 +12,6 @@ class Menu {
String& getItemAt(size_t index); String& getItemAt(size_t index);
size_t getCurrentItemIndex() const; size_t getCurrentItemIndex() const;
private: private:
String items[MENU_ITEM_COUNT]; String items[LCD_ROWS];
int currentItem; int currentItem;
}; };
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <Arduino.h>
#include "constants.hpp"
class Pet {
public:
Pet();
bool isAlive;
void updateHunger(int8_t delta);
void updateJoy(int8_t delta);
void updateEnergy(int8_t delta);
void updateCleanliness(int8_t delta);
int8_t getHunger() const;
int8_t getJoy() const;
int8_t getEnergy() const;
int8_t getCleanliness() const;
String getReasonForDeath() const;
byte getAnimationFrame();
private:
int8_t hunger;
int8_t joy;
int8_t energy;
int8_t cleanliness;
String reasonForDeath;
byte lastAnimationFrame;
uint64_t lastAnimationFrameTime;
};
-1
View File
@@ -18,5 +18,4 @@ monitor_speed = 9600
; External libraries ; External libraries
lib_deps = lib_deps =
embeddedartistry/LibPrintf@1.2.13
marcoschwartz/LiquidCrystal_I2C@1.1.4 marcoschwartz/LiquidCrystal_I2C@1.1.4
+50 -7
View File
@@ -1,11 +1,36 @@
#include "display.hpp" #include "display.hpp"
// Initialize the LCD display with the specified number of columns and rows, using the I2C address. // Initialize the LCD display with the specified number of columns and rows, using the I2C address.
Display::Display(uint8_t addr, uint8_t cols, uint8_t rows) : rows(rows), cols(cols), lcd(addr, cols, rows) {} Display::Display() : lcd(LCD_I2C_ADDRESS, LCD_COLS, LCD_ROWS) {}
void Display::begin() { void Display::begin() {
byte CUSTOM_CHAR_PET1[] = {
B00100,
B01110,
B11111,
B10101,
B11111,
B11111,
B10001,
B11111
};
byte CUSTOM_CHAR_PET2[] = {
B00100,
B01110,
B11111,
B10101,
B11111,
B10001,
B11011,
B11111
};
lcd.init(); lcd.init();
lcd.backlight(); lcd.backlight();
// Create a custom character for the pet
lcd.createChar(0, CUSTOM_CHAR_PET1);
lcd.createChar(1, CUSTOM_CHAR_PET2);
} }
void Display::clear() { void Display::clear() {
@@ -14,23 +39,41 @@ void Display::clear() {
void Display::drawBuffer(String buffer[]) { void Display::drawBuffer(String buffer[]) {
clear(); clear();
for (size_t i = 0; i < rows; i++) { for (size_t i = 0; i < LCD_ROWS; i++) {
lcd.setCursor(0, i); lcd.setCursor(0, i);
lcd.print(buffer[i].substring(0, cols)); // Ensure we only print up to the number of columns lcd.print(buffer[i].substring(0, LCD_COLS)); // Ensure we only print up to the number of columns
} }
} }
void Display::drawPetStats(Pet &pet) {
clear();
lcd.setCursor(0, 0);
lcd.print("Hunger: " + String(pet.getHunger()));
lcd.setCursor(0, 1);
lcd.print("Joy: " + String(pet.getJoy()));
lcd.setCursor(0, 2);
lcd.print("Energy: " + String(pet.getEnergy()));
lcd.setCursor(0, 3);
lcd.print("Cleanliness: " + String(pet.getCleanliness()));
}
void Display::drawPet(Pet &pet) {
lcd.setCursor(LCD_COLS - 2, LCD_ROWS / 2 - 1); // Position the pet
lcd.write(pet.getAnimationFrame()); // Draw the custom pet character
}
void Display::drawMenu(Menu &menu) { void Display::drawMenu(Menu &menu) {
clear(); clear();
size_t currentItemIndex = menu.getCurrentItemIndex(); size_t currentItemIndex = menu.getCurrentItemIndex();
for (size_t i = 0; i < MENU_ITEM_COUNT; i++) { for (size_t i = 0; i < LCD_ROWS; i++) {
lcd.setCursor(0, i); lcd.setCursor(0, i);
String item = menu.getItemAt(i); String item = menu.getItemAt(i);
if (i == currentItemIndex) { if (i == currentItemIndex) {
lcd.print("> " + item); lcd.print(("> " + item).substring(0, LCD_COLS)); // Add a ">" to indicate the current item
} else { } else {
lcd.print(" " + item); lcd.print((" " + item).substring(0, LCD_COLS)); // Indent non-selected items
} }
} }
} }
+52 -49
View File
@@ -2,14 +2,10 @@
Game::Game() : joystick(), display(), menu() { Game::Game() : joystick(), display(), menu() {
state = (GameState) { state = (GameState) {
.dead = false, .pet = Pet(),
.menuOpen = false,
.hunger = 0,
.joy = 100,
.energy = 100,
.cleanliness = 100,
.lastActionTime = 0, .lastActionTime = 0,
.shouldClear = false, .isMenuOpen = false,
.shouldClearDisplay = true,
}; };
String items[] = { String items[] = {
@@ -32,14 +28,14 @@ void Game::begin() {
void Game::update() { void Game::update() {
// If the pet is dead, we don't need to update its state // If the pet is dead, we don't need to update its state
if (state.dead) { if (!state.pet.isAlive) {
return; return;
} }
bool isPressed = joystick.isPressed(); bool isPressed = joystick.isPressed();
// If the menu is open, we don't need to update the pet's state // If the menu is open, we don't need to update the pet's state
if (state.menuOpen) { if (state.isMenuOpen) {
JoystickDirection direction = joystick.getDirection(); JoystickDirection direction = joystick.getDirection();
if (menu.updateCurrentItem(direction)) { if (menu.updateCurrentItem(direction)) {
forceUpdate("Menu navigation"); forceUpdate("Menu navigation");
@@ -47,7 +43,7 @@ void Game::update() {
// If the joystick is pressed, execute the current menu item // If the joystick is pressed, execute the current menu item
if (isPressed) { if (isPressed) {
state.menuOpen = false; state.isMenuOpen = false;
// Based on the current menu item, perform the corresponding action // Based on the current menu item, perform the corresponding action
switch (menu.getCurrentItemIndex()) { switch (menu.getCurrentItemIndex()) {
case 0: case 0:
@@ -70,7 +66,7 @@ void Game::update() {
// If the joystick is pressed and the menu is not open, open the menu // If the joystick is pressed and the menu is not open, open the menu
if (isPressed) { if (isPressed) {
state.menuOpen = true; state.isMenuOpen = true;
forceUpdate("Opening menu"); forceUpdate("Opening menu");
return; return;
} }
@@ -78,74 +74,81 @@ void Game::update() {
// Update the pet's stats based on the time elapsed since the last action // Update the pet's stats based on the time elapsed since the last action
uint64_t currentTime = millis(); uint64_t currentTime = millis();
if (currentTime - state.lastActionTime >= ACTION_INTERVAL) { if (currentTime - state.lastActionTime >= ACTION_INTERVAL) {
state.hunger += 10; state.pet.updateHunger(10);
state.joy -= 5; state.pet.updateJoy(-5);
state.energy -= 5; state.pet.updateEnergy(-5);
state.cleanliness -= 5; state.pet.updateCleanliness(-5);
state.lastActionTime = currentTime; state.lastActionTime = currentTime;
forceUpdate("Time-based stat update"); forceUpdate("Time-based stat update");
} }
// Check if the pet has died from any of the conditions
bool starvedToDeath = state.hunger >= MAXIMUM_STAT;
bool diedOfBoredom = state.joy <= -MAXIMUM_STAT;
bool diedOfExhaustion = state.energy <= -MAXIMUM_STAT;
bool diedOfFilth = state.cleanliness <= -MAXIMUM_STAT;
state.dead = starvedToDeath || diedOfBoredom || diedOfExhaustion || diedOfFilth;
} }
void Game::render() { void Game::render() {
if (state.shouldClear) { // If the display doesn't need to be cleared, we can skip rendering
display.clear(); if (!state.shouldClearDisplay) {
state.shouldClear = false; // However, we still need to draw the pet if it's alive and
// we're not in the menu
if (state.menuOpen) { if (state.pet.isAlive && !state.isMenuOpen) {
display.drawMenu(menu); display.drawPet(state.pet);
Serial.println("Rendering menu");
return;
} }
}
if (state.menuOpen) {
return; return;
} }
if (state.dead) { state.shouldClearDisplay = false;
display.getLCD().print("Your pet has died.");
if (state.isMenuOpen) {
display.drawMenu(menu);
Serial.println("Rendering menu");
return; return;
} }
display.getLCD().setCursor(0, 0); // If the pet is dead, display a message and return
display.getLCD().print("H: " + String(state.hunger)); if (!state.pet.isAlive) {
display.getLCD().setCursor(0, 1); String buffer[LCD_ROWS] = {
display.getLCD().print("J: " + String(state.joy)); "Your pet has died of",
display.getLCD().setCursor(0, 2); state.pet.getReasonForDeath() + ".",
display.getLCD().print("E: " + String(state.energy)); "Reset the device",
display.getLCD().setCursor(0, 3); "to start over."
display.getLCD().print("C: " + String(state.cleanliness)); };
display.drawBuffer(buffer);
return;
}
// Render the pet's stats on the display
display.drawPetStats(state.pet);
} }
void Game::forceUpdate(String reason) { void Game::forceUpdate(String reason) {
if (!state.shouldClear) { if (!state.shouldClearDisplay) {
Serial.println("Forcing update: " + reason); Serial.println("Forcing update: " + reason);
state.shouldClear = true; state.shouldClearDisplay = true;
} }
} }
void Game::feed() { void Game::feed() {
state.hunger += 20; if (state.pet.isAlive) {
state.pet.updateHunger(-20);
}
} }
void Game::play() { void Game::play() {
state.joy += 20; if (state.pet.isAlive) {
state.pet.updateJoy(20);
}
} }
void Game::sleep() { void Game::sleep() {
state.energy += 20; if (state.pet.isAlive) {
state.pet.updateEnergy(20);
}
} }
void Game::clean() { void Game::clean() {
state.cleanliness += 20; if (state.pet.isAlive) {
state.pet.updateCleanliness(20);
}
} }
+31 -4
View File
@@ -1,10 +1,37 @@
#include "joystick.hpp" #include "joystick.hpp"
Joystick::Switch::Switch(uint8_t pin) : pin(pin), lastKnownState(HIGH), lastStableState(HIGH), lastDebounceTime(0) {
pinMode(pin, INPUT_PULLUP); // Use internal pull-up resistor
}
bool Joystick::Switch::isPressed() {
bool state = digitalRead(pin);
// Check for state change and debounce
if (state != lastKnownState) {
lastDebounceTime = millis(); // Reset debounce timer
lastKnownState = state;
}
// If the state has been stable for longer than the debounce delay, update the stable state
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
if (lastStableState != state) {
lastStableState = state; // Update stable state
return state == LOW; // Return true if the button is pressed (active LOW)
}
}
return false; // No change in button state
}
// Initialize the joystick pins in the constructor // Initialize the joystick pins in the constructor
Joystick::Joystick() : vrx(A0), vry(A1), sw(4) { Joystick::Joystick() : vrx(A0), vry(A1), sw(Switch(4)) {
pinMode(vrx, INPUT); pinMode(vrx, INPUT);
pinMode(vry, INPUT); pinMode(vry, INPUT);
pinMode(sw, INPUT_PULLUP);
// TODO: Calibrate the joystick by reading the center position and storing it as an offset
xOffset = 10;
yOffset = 13;
} }
// Map the analog readings from the joystick to a range of -100 to 100 for both X and Y axes // Map the analog readings from the joystick to a range of -100 to 100 for both X and Y axes
@@ -17,8 +44,8 @@ double Joystick::getY() const {
} }
// Check if the joystick button is pressed (active LOW) // Check if the joystick button is pressed (active LOW)
bool Joystick::isPressed() const { bool Joystick::isPressed() {
return digitalRead(sw) == LOW; return sw.isPressed();
} }
// Determine the direction of the joystick based on the X and Y values, considering a deadzone to prevent jitter // Determine the direction of the joystick based on the X and Y values, considering a deadzone to prevent jitter
+3 -3
View File
@@ -4,7 +4,7 @@ Menu::Menu() : currentItem(0) {}
void Menu::setItems(String* items) { void Menu::setItems(String* items) {
this->currentItem = 0; this->currentItem = 0;
for (int i = 0; i < MENU_ITEM_COUNT; i++) { for (int i = 0; i < LCD_ROWS; i++) {
this->items[i] = items[i]; this->items[i] = items[i];
} }
} }
@@ -12,10 +12,10 @@ void Menu::setItems(String* items) {
bool Menu::updateCurrentItem(JoystickDirection &direction) { bool Menu::updateCurrentItem(JoystickDirection &direction) {
switch (direction) { switch (direction) {
case JoystickDirection::UP: case JoystickDirection::UP:
currentItem = (currentItem - 1 + MENU_ITEM_COUNT) % MENU_ITEM_COUNT; currentItem = (currentItem - 1 + LCD_ROWS) % LCD_ROWS;
return true; return true;
case JoystickDirection::DOWN: case JoystickDirection::DOWN:
currentItem = (currentItem + 1) % MENU_ITEM_COUNT; currentItem = (currentItem + 1) % LCD_ROWS;
return true; return true;
default: default:
return false; return false;
+103
View File
@@ -0,0 +1,103 @@
#include "pet.hpp"
Pet::Pet() :
isAlive(true),
hunger(0),
joy(100),
energy(100),
cleanliness(100),
reasonForDeath(""),
lastAnimationFrame(0),
lastAnimationFrameTime(0)
{}
void Pet::updateHunger(int8_t delta) {
hunger += delta;
if (hunger < 0) {
isAlive = false;
reasonForDeath = "overfeeding";
return;
}
if (hunger > MAXIMUM_STAT) {
isAlive = false;
reasonForDeath = "starvation";
}
}
void Pet::updateJoy(int8_t delta) {
joy += delta;
if (joy < 0) {
isAlive = false;
reasonForDeath = "depression";
return;
}
if (joy > MAXIMUM_STAT) {
isAlive = false;
reasonForDeath = "overstimulation";
}
}
void Pet::updateEnergy(int8_t delta) {
energy += delta;
if (energy < 0) {
isAlive = false;
reasonForDeath = "exhaustion";
return;
}
if (energy > MAXIMUM_STAT) {
isAlive = false;
reasonForDeath = "overresting";
}
}
void Pet::updateCleanliness(int8_t delta) {
cleanliness += delta;
if (cleanliness < 0) {
isAlive = false;
reasonForDeath = "disease";
return;
}
if (cleanliness > MAXIMUM_STAT) {
isAlive = false;
reasonForDeath = "overcleaning";
}
}
int8_t Pet::getHunger() const {
return hunger;
}
int8_t Pet::getJoy() const {
return joy;
}
int8_t Pet::getEnergy() const {
return energy;
}
int8_t Pet::getCleanliness() const {
return cleanliness;
}
String Pet::getReasonForDeath() const {
return reasonForDeath;
}
byte Pet::getAnimationFrame() {
uint64_t currentTime = millis();
if (currentTime - lastAnimationFrameTime >= ANIMATION_FRAME_INTERVAL) {
lastAnimationFrameTime = currentTime;
lastAnimationFrame = (lastAnimationFrame + 1) % 2; // Alternate between frame 0 and 1
}
return lastAnimationFrame;
}