#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
#define SPEAKER 6
#define ENCODER_CLICK 3
#define ENCODER_CLK 2
#define ENCODER_DT 4
#define BUTTON_SHOOT 5
#define EMPTY_PIN 13
#define SCREEN_WIDTH 16
#define SCREEN_HEIGHT 2
#define FRAME_TIME 50
// z, x, c, v, ... will be replaced with custom characters
// escape sequences are not used here, because it would be not readable
String alo[4][2] = {
{
"> 4.01 | 4.02 4.03 4.04 4.05 4.06 | WC <",
"> zz c c x | x x x x x | zz c c x x x <"
},
{
"> 3.01 | 3.02 3.03 3.04 3.05 3.06 | WC <",
"> zz c c x | x x x v x | zz c c x x x <"
},
{
"> | 2.04 2.05 2.06 2.07 2.08 | WC <",
"> zz c c x v | x x x x x | zz c c x x x <"
},
{
// "> o oo o o o o oo oo o oo o ooo oo o o o o o o o o ooo ooooo o ooo<",
// ">zzzzzzzzzzzzzzzxxzxxzzzzzzzzzzzzzzzzzzzzzzzzzxxzxxzzzzzzzzzzzzzzzzzzzzzzz | | | <"
// not enough RAM
">-xx z oo o o o o oo oo <",
"> xv zzzzzzzzzzxxzxxzzzzzzzzzz<"
}
};
#define MAX_FLOOR 4
#define MAX_ENEMIES 5
// Maps floor numbers to floor in alo array
// -1 - floor not exists
int floor_map[MAX_FLOOR+1] = {
3,
-1,
2,
1,
0
};
enum UIMode {
game,
message,
elevator,
computer,
gameover,
end_animation,
end_message
};
struct GameState {
int alo_floor;
int x_view_position;
int x_player_position;
UIMode mode;
int message_viewpos;
String message_text;
UIMode mode_before_message;
int message_height;
int world_exit_try_number;
bool elevator_started;
int elevator_time;
int new_floor;
int shoot_x;
int shoot_v;
int last_direction;
int shoot_timeout;
int time; // int overflow should be no problem here
};
// enemies positions
int enemies[3][MAX_ENEMIES];
GameState state;
struct ComputerState {
int menu_selection;
};
ComputerState computer_state;
bool password_known = false;
bool polinka_enabled = false;
bool event_encoder_l = false;
bool event_encoder_r = false;
bool event_encoder_click = false;
uint8_t block[8] = {
0b11111,
0b11111,
0b11111,
0b11111,
0b11111,
0b11111,
0b11111,
0b11111,
};
uint8_t transparent_block[8] = {
0b10101,
0b01010,
0b10101,
0b01010,
0b10101,
0b01010,
0b10101,
0b01010,
};
uint8_t empty_block[8] = {
0b11111,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b10001,
0b11111,
};
uint8_t player_char[8] = {
0b01110,
0b01110,
0b00100,
0b01110,
0b00100,
0b01110,
0b01010,
0b01010,
};
char replaceChar(char c) {
switch(c) {
case 'z':
return 4;
case 'x':
return 3;
case 'c':
return 160; // Elevator doors
case 'v':
return 6;
}
return c;
}
// Framebuffer
// This allows for updating screen once per frame
// to avoid blinking.
char framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH];
void fb_init() {
for (int i = 0; i < SCREEN_HEIGHT; i++) {
for (int j = 0; j < SCREEN_WIDTH; j++) {
framebuffer[i][j] = ' ';
}
}
}
void fb_toscreen() {
for (int i = 0; i < SCREEN_HEIGHT; i++) {
for (int j = 0; j < SCREEN_WIDTH; j++) {
lcd.setCursor(j, i);
lcd.write(framebuffer[i][j]);
}
}
}
void fb_write(int x, int y, char c) {
if (framebuffer[y][x] != c) {
framebuffer[y][x] = c;
}
}
void fb_print(int x, int y, String s) {
for (char c : s) {
fb_write(x, y, c);
x++;
}
}
// Input functions
void readEncoder() {
int dt = digitalRead(ENCODER_DT);
if (dt == HIGH) {
// ->
event_encoder_r = true;
}
if (dt == LOW) {
// <-
event_encoder_l = true;
}
}
void encoderClick() {
event_encoder_click = true;
}
void handle_encoder(int direction) {
switch(state.mode) {
case UIMode::game:
move_player(direction);
break;
case UIMode::message:
scroll_message(direction);
break;
case UIMode::elevator:
change_floor(direction);
break;
case UIMode::computer:
if (computer_state.menu_selection + direction >= 0 && computer_state.menu_selection + direction <= 2) {
computer_state.menu_selection += direction;
}
break;
}
}
void handle_game_click() {
char object = alo[floor_map[state.alo_floor]][1][state.x_player_position];
if ((state.alo_floor != 0 && object == 'c') || (state.alo_floor == 0 && object == 'x' && state.x_player_position > 4)) {
enter_elevator();
}
if (state.alo_floor == 0 && object == 'v') {
if (polinka_enabled) {
end_game();
} else {
display_message("Nieczynne");
}
}
if (state.alo_floor == 2 && state.x_player_position == 16) {
display_message("W serwerowni\njest haslo\nalo12345");
password_known = true;
}
if (state.alo_floor == 3 && state.x_player_position == 42) {
if (password_known) {
display_computer();
} else {
display_message("Nie znasz hasla");
}
}
}
void shoot() {
state.shoot_x = state.x_player_position;
state.shoot_v = state.last_direction;
state.shoot_timeout = 20;
tone(SPEAKER, 1500, 100);
}
void move_player(int delta_x) {
const int min_margin = 2;
int new_pos = state.x_player_position + delta_x;
if (new_pos <= 0 || new_pos >= alo[floor_map[state.alo_floor]][0].length()-1) {
tone(SPEAKER, 100, 20);
state.world_exit_try_number++;
if (state.world_exit_try_number == 10) {
display_message("Strzalki\noznaczaja koniec\nswiatu");
state.world_exit_try_number = 0;
}
return;
}
if (new_pos < state.x_view_position + min_margin && state.x_view_position > 0){
state.x_view_position--;
}
if (new_pos > state.x_view_position + SCREEN_WIDTH - min_margin - 1
&& state.x_view_position + SCREEN_WIDTH < alo[floor_map[state.alo_floor]][0].length()) {
state.x_view_position++;
}
// Serial.println(new_pos);
state.last_direction = delta_x;
tone(SPEAKER, 200, 20);
state.x_player_position = new_pos;
}
void scroll_message(int delta_pos) {
int new_pos = state.message_viewpos + delta_pos;
if (new_pos < 0 || new_pos >= state.message_height ) {
return;
}
state.message_viewpos = new_pos;
}
void change_floor(int delta_floor) {
if (state.elevator_started) {
return;
}
if (state.new_floor + delta_floor <= MAX_FLOOR && state.new_floor + delta_floor >= 0) {
state.new_floor += delta_floor;
tone(SPEAKER, 200, 20);
} else {
tone(SPEAKER, 100, 20);
}
}
void start_elevator() {
if (state.elevator_started) {
return;
}
if (floor_map[state.new_floor] != -1) {
state.elevator_started = true;
} else {
display_message("To nie jest\nteren ALO!");
}
}
void computer_selection() {
switch(computer_state.menu_selection) {
case 0:
display_message("1:Znaleziono\nklucze\n2:siatkowka");
break;
case 1:
display_message("Wlaczono");
polinka_enabled = true;
break;
case 2:
state.mode = game;
tone(SPEAKER, 1000, 500);
delay(500);
tone(SPEAKER, 200, 500);
break;
}
}
// UI mode changing functions
void enter_elevator() {
state.mode = elevator;
state.new_floor = state.alo_floor;
state.elevator_started = false;
state.elevator_time = 0;
}
void display_computer() {
state.mode = computer;
computer_state.menu_selection = 0;
tone(SPEAKER, 200, 500);
delay(500);
tone(SPEAKER, 1000, 500);
}
void exit_elevator() {
if (state.new_floor != 0 && state.alo_floor == 0) {
state.x_player_position = 8;
state.x_view_position = 3;
}
if (state.alo_floor != 0 && state.new_floor == 0) {
state.x_player_position = 17;
state.x_view_position = 10;
}
state.alo_floor = state.new_floor;
state.mode = game;
}
void close_message() {
// If message scrolled to bottom
if (state.message_viewpos == state.message_height - 1) {
state.mode = state.mode_before_message;
tone(SPEAKER, 5000, 50);
}
}
void end_game() {
state.time = 0;
state.mode = end_animation;
}
void display_message(String message_text) {
state.mode_before_message = state.mode;
state.mode = message;
state.message_text = message_text;
state.message_viewpos = 0;
state.message_height = 1;
for (char c : message_text) {
if (c == '\n') {
state.message_height++;
}
}
tone(SPEAKER, 60, 400);
}
// Rendering functions
void render_alo() {
int start_x = state.x_view_position;
for (int x = 0; x < SCREEN_WIDTH; x++) {
for (int y = 0; y < SCREEN_HEIGHT; y++) {
if (y == 0 || x != state.x_player_position - start_x) {
char c = replaceChar(alo[floor_map[state.alo_floor]][y][x+start_x]);
fb_write(x, y, c);
}
}
}
}
void render_player() {
fb_write(state.x_player_position - state.x_view_position, 1, 5);
}
void render_elevator_dialog() {
fb_init();
fb_print(0, 0, "Winda");
fb_write(15, 1, '0' + state.new_floor);
if (state.elevator_started) {
fb_write(0, 1, '[');
fb_write(1+state.elevator_time%4, 1, '*');
fb_write(5, 1, ']');
// Serial.println("winda jedzie");
if (state.elevator_time%15 == 0) {
// Serial.println("winda piszczy, bo jedzie");
tone(SPEAKER, 1100, 100);
}
state.elevator_time++;
if (state.elevator_time > 15*abs(state.new_floor - state.alo_floor)) {
exit_elevator();
}
}
}
void render_message() {
int screen_x = 0;
fb_init();
int line_num = 0;
for (char c : state.message_text) {
bool line_visible = (line_num >= state.message_viewpos && line_num < state.message_viewpos + SCREEN_HEIGHT);
if (c == '\n') {
line_num++;
screen_x = 0;
continue;
}
if (line_visible) {
fb_write(screen_x, line_num - state.message_viewpos, c);
screen_x++;
}
}
if (state.message_viewpos == state.message_height - 1) {
fb_print(0, 1, "[Zamknij]");
}
}
void render_enemies() {
if (floor_map[state.alo_floor] < 3) {
for (int i = 0; i < MAX_ENEMIES; i++) {
int pos = enemies[floor_map[state.alo_floor]][i];
int screen_pos = pos - state.x_view_position;
if (pos == state.shoot_x) {
enemies[floor_map[state.alo_floor]][i] = random(1, alo[state.alo_floor][0].length()-1);
state.shoot_x = -1;
continue;
}
if (pos == state.x_player_position) {
state.mode = gameover; // comment this line for testing
return;
}
if (screen_pos >= 0 && screen_pos < SCREEN_WIDTH) {
fb_write(screen_pos, 1, '#');
}
if (state.time%4 == 0 && abs(state.x_player_position - pos) < 8) {
if (pos > state.x_player_position) {
enemies[floor_map[state.alo_floor]][i]--;
} else {
enemies[floor_map[state.alo_floor]][i]++;
}
}
}
}
}
void render_projectile() {
if (state.shoot_x == -1) {
return;
}
state.shoot_x += state.shoot_v;
if (state.shoot_x <= 0 || state.shoot_x >= alo[floor_map[state.alo_floor]][0].length()-1
|| abs(state.shoot_x - state.x_player_position) > 16) {
state.shoot_x = -1;
return 0;
}
int onscreen_position = state.shoot_x - state.x_view_position;
if (onscreen_position < 0 || onscreen_position >= SCREEN_WIDTH) {
return;
}
if (state.shoot_v == -1) {
fb_write(onscreen_position, 1, '<');
} else {
fb_write(onscreen_position, 1, '>');
}
}
void render_gameover() {
String text = "KONIEC!";
fb_init();
fb_print(state.time%(SCREEN_WIDTH-text.length()+1), random(0, 2), text);
}
void render_computer() {
fb_init();
fb_print(0, 0, "Komputer");
int sel = computer_state.menu_selection;
if (sel == 0) {
fb_print(1, 1, "Librus");
fb_write(SCREEN_WIDTH-1, 1, '>');
} else if (sel == 1) {
fb_print(0, 1, "<Wlacz polinke");
fb_write(SCREEN_WIDTH-1, 1, '>');
} else {
fb_print(0, 1, "<Wylacz");
}
}
void render_polinka() {
fb_init();
fb_write(0, 0, 3);
fb_write(0, 1, 3);
fb_write(SCREEN_WIDTH-1, 0, 3);
fb_write(SCREEN_WIDTH-1, 1, 3);
for (int i = 1; i < SCREEN_WIDTH-1; i++) {
fb_write(i, 0, '_');
}
if (SCREEN_WIDTH-1-state.time/4 <= 0) {
state.mode = end_message;
return 0;
}
fb_write(SCREEN_WIDTH-1-state.time/4, 1, 127);
}
void render_end_message() {
fb_init();
fb_print(0, 0, "Wygrales");
}
void setup() {
Serial.begin(9600);
lcd.begin(SCREEN_WIDTH, SCREEN_HEIGHT);
lcd.createChar(3, block);
lcd.createChar(4, transparent_block);
lcd.createChar(5, player_char);
lcd.createChar(6, empty_block);
fb_init();
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_CLICK, INPUT_PULLUP);
pinMode(BUTTON_SHOOT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), readEncoder, FALLING);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLICK), encoderClick, FALLING);
// Read noise from unconnected pin and use it as seed
randomSeed(analogRead(EMPTY_PIN));
state.world_exit_try_number = 0;
state.x_view_position = 0;
state.x_player_position = 7;
state.alo_floor = 4;
state.shoot_x = -1;
state.shoot_timeout = 0;
state.last_direction = 1;
state.time = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < MAX_ENEMIES; j++) {
enemies[i][j] = random(16, alo[i][0].length()-1);
}
}
}
void loop() {
// Game main loop
// Events processing
if (event_encoder_l) {
handle_encoder(-1);
event_encoder_l = false;
}
if (event_encoder_r) {
handle_encoder(1);
event_encoder_r = false;
}
if (event_encoder_click) {
switch(state.mode) {
case message:
close_message();
break;
case game:
handle_game_click();
break;
case elevator:
start_elevator();
break;
case computer:
computer_selection();
break;
}
event_encoder_click = false;
}
if (state.mode == UIMode::game) {
if (state.shoot_timeout == 0) {
if (digitalRead(BUTTON_SHOOT) == LOW) {
shoot();
}
} else {
state.shoot_timeout--;
}
}
state.time++;
// Rendering
switch(state.mode) {
case UIMode::game:
render_alo();
render_player();
render_projectile();
render_enemies();
break;
case UIMode::message:
render_message();
break;
case UIMode::elevator:
render_elevator_dialog();
break;
case UIMode::computer:
render_computer();
break;
case UIMode::gameover:
render_gameover();
break;
case UIMode::end_animation:
render_polinka();
break;
case UIMode::end_message:
render_end_message();
break;
}
fb_toscreen();
delay(FRAME_TIME);
}