/*
* ====================================================
* GRA SIMON SAYS DLA DZIECI - WERSJA ROZBUDOWANA
* ====================================================
* ESP32 + LCD1602 + WiFi AP + Panel WWW
* ====================================================
*/
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "esp_random.h"
// ============ KONFIGURACJA WiFi AP ============
const char* AP_SSID = "Powtorz-kolory";
const char* AP_PASS = "kubaimilosz";
// ============ KONFIGURACJA PINÓW ============
#define LED_CZERWONA 2
#define LED_ZIELONA 4
#define LED_NIEBIESKA 5
#define LED_ZOLTA 18
#define BTN_CZERWONY 13 // Poprzedni gracz
#define BTN_ZIELONY 12 // Następny gracz
#define BTN_NIEBIESKI 14 // Start gry
#define BTN_ZOLTY 27 // Start gry
#define BUZZER 15
// ============ USTAWIENIA GRY ============
#define MAX_POZIOM 20
#define MAX_GRACZY 10
#define MAX_IMIE_LEN 20
#define MAX_TEXT_LEN 250
// ============ DŹWIĘKI ============
#define TON_CZERWONY 262
#define TON_ZIELONY 330
#define TON_NIEBIESKI 392
#define TON_ZOLTY 523
#define TON_SUKCES 880
#define TON_PORAZKA 150
// ============ OBIEKTY ============
LiquidCrystal_I2C lcd(0x27, 16, 2);
WebServer server(80);
Preferences preferences;
// ============ ZMIENNE GRY ============
int sekwencja[MAX_POZIOM];
int poziom = 0;
int krokGracza = 0;
bool graAktywna = false;
unsigned long seedDodatkowy = 0;
// Tablice pinów
int ledy[] = {LED_CZERWONA, LED_ZIELONA, LED_NIEBIESKA, LED_ZOLTA};
int przyciski[] = {BTN_CZERWONY, BTN_ZIELONY, BTN_NIEBIESKI, BTN_ZOLTY};
int tony[] = {TON_CZERWONY, TON_ZIELONY, TON_NIEBIESKI, TON_ZOLTY};
// ============ DANE GRACZY ============
struct Gracz {
char imie[MAX_IMIE_LEN + 1];
int najlepszyWynik;
};
Gracz gracze[MAX_GRACZY];
int liczbaGraczy = 0;
int aktualnyGracz = 0;
int ostatniGracz = 0;
// ============ USTAWIENIA ============
int czasPokazu = 600; // ms
int czasPrzerwy = 300; // ms
int czasOczekiwania = 5000; // ms
int predkoscPrzewijania = 300; // ms
// ============ TEKST NA LCD ============
char tekstDoWyswietlenia[MAX_TEXT_LEN + 1] = "";
bool wyswietlanieTekstu = false;
unsigned long czasStartuTekstu = 0;
int pozycjaPrzewijania = 0;
unsigned long ostatniePrzewijanie = 0;
// ============ WŁASNE ZNAKI LCD ============
byte buzkaUsmiech[] = {B00000, B01010, B01010, B00000, B10001, B01110, B00000, B00000};
byte buzkaSmutek[] = {B00000, B01010, B01010, B00000, B01110, B10001, B00000, B00000};
byte gwiazdka[] = {B00100, B01110, B11111, B01110, B01010, B10001, B00000, B00000};
byte serce[] = {B00000, B01010, B11111, B11111, B01110, B00100, B00000, B00000};
byte strzalkaL[] = {B00010, B00110, B01110, B11110, B01110, B00110, B00010, B00000};
byte strzalkaP[] = {B01000, B01100, B01110, B01111, B01110, B01100, B01000, B00000};
// ============ STRONA HTML ============
const char STRONA_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simon Says - Panel</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
font-size: 2em;
}
.panel {
background: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.panel h2 {
color: #667eea;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
font-size: 1.3em;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input[type="text"], input[type="number"], textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
border-color: #667eea;
outline: none;
}
textarea {
resize: vertical;
min-height: 100px;
}
button {
padding: 12px 25px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: transform 0.2s, box-shadow 0.2s;
margin: 5px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
button:active {
transform: translateY(0);
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #11998e, #38ef7d);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #eb3349, #f45c43);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: white;
}
.btn-small {
padding: 8px 15px;
font-size: 14px;
}
.gracze-lista {
max-height: 300px;
overflow-y: auto;
}
.gracz-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 8px;
}
.gracz-item:hover {
background: #e9ecef;
}
.gracz-info {
flex: 1;
}
.gracz-imie {
font-weight: bold;
color: #333;
}
.gracz-wynik {
color: #667eea;
font-size: 0.9em;
}
.wyniki-tabela {
width: 100%;
border-collapse: collapse;
}
.wyniki-tabela th, .wyniki-tabela td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.wyniki-tabela th {
background: #667eea;
color: white;
}
.wyniki-tabela tr:nth-child(even) {
background: #f8f9fa;
}
.wyniki-tabela tr:hover {
background: #e9ecef;
}
.medal {
font-size: 1.5em;
}
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider-container input[type="range"] {
flex: 1;
height: 8px;
-webkit-appearance: none;
background: #ddd;
border-radius: 4px;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
}
.slider-value {
min-width: 60px;
text-align: center;
font-weight: bold;
color: #667eea;
}
.status-box {
padding: 15px;
border-radius: 8px;
margin-top: 15px;
text-align: center;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-info {
background: #cce5ff;
color: #004085;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
}
button {
width: 100%;
margin: 5px 0;
}
}
.aktualny-gracz {
text-align: center;
padding: 15px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 10px;
margin-bottom: 15px;
}
.aktualny-gracz h3 {
font-size: 1.5em;
}
.char-counter {
text-align: right;
font-size: 0.9em;
color: #666;
margin-top: 5px;
}
.buttons-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<h1>🎮 Simon Says - Panel Sterowania</h1>
<!-- AKTUALNY GRACZ -->
<div class="panel">
<div class="aktualny-gracz">
<h3>Aktualny gracz: <span id="aktualnyGraczNazwa">---</span></h3>
<p>Najlepszy wynik: <span id="aktualnyGraczWynik">0</span> ⭐</p>
</div>
</div>
<!-- WIADOMOŚĆ NA LCD -->
<div class="panel">
<h2>📺 Wiadomość na ekranie</h2>
<div class="form-group">
<label for="tekstLCD">Tekst do wyświetlenia:</label>
<textarea id="tekstLCD" maxlength="250" placeholder="Wpisz wiadomość..."></textarea>
<div class="char-counter"><span id="charCount">0</span>/250 znaków</div>
</div>
<div class="form-group slider-container">
<label>Prędkość przewijania:</label>
<input type="range" id="predkoscSlider" min="100" max="800" value="300">
<span class="slider-value" id="predkoscValue">300ms</span>
</div>
<div class="buttons-row">
<button class="btn-success" onclick="wyslijTekst()">📤 WYŚLIJ</button>
<button class="btn-danger" onclick="stopTekst()">⏹️ STOP</button>
<button class="btn-warning" onclick="wyczyscTekst()">🗑️ WYCZYŚĆ</button>
</div>
<div id="statusTekst" class="status-box status-info" style="display:none;"></div>
</div>
<!-- ZARZĄDZANIE GRACZAMI -->
<div class="panel">
<h2>👥 Zarządzanie graczami</h2>
<div class="form-group">
<label for="noweImie">Dodaj nowego gracza:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="noweImie" maxlength="20" placeholder="Imię (max 20 znaków)">
<button class="btn-primary" onclick="dodajGracza()">➕ Dodaj</button>
</div>
</div>
<div class="gracze-lista" id="listaGraczy">
<!-- Lista graczy -->
</div>
</div>
<!-- TABELA WYNIKÓW -->
<div class="panel">
<h2>🏆 Tabela wyników</h2>
<table class="wyniki-tabela">
<thead>
<tr>
<th>Miejsce</th>
<th>Imię</th>
<th>Wynik</th>
</tr>
</thead>
<tbody id="tabelaWynikow">
<!-- Wyniki -->
</tbody>
</table>
</div>
<!-- USTAWIENIA GRY -->
<div class="panel">
<h2>⚙️ Ustawienia gry</h2>
<div class="grid-2">
<div class="form-group">
<label for="czasPokazu">Czas świecenia LED (ms):</label>
<input type="number" id="czasPokazu" min="200" max="2000" value="600">
</div>
<div class="form-group">
<label for="czasPrzerwy">Przerwa między kolorami (ms):</label>
<input type="number" id="czasPrzerwy" min="100" max="1000" value="300">
</div>
<div class="form-group">
<label for="czasOczekiwania">Czas na odpowiedź (ms):</label>
<input type="number" id="czasOczekiwania" min="2000" max="15000" value="5000">
</div>
</div>
<button class="btn-primary" onclick="zapiszUstawienia()">💾 Zapisz ustawienia</button>
<div id="statusUstawienia" class="status-box status-success" style="display:none;">Zapisano!</div>
</div>
<!-- STATUS GRY -->
<div class="panel">
<h2>📊 Status gry</h2>
<div id="statusGry" class="status-box status-info">
Ładowanie...
</div>
<button class="btn-info" onclick="odswiezStatus()" style="margin-top: 15px;">🔄 Odśwież</button>
</div>
</div>
<script>
// Aktualizacja licznika znaków
document.getElementById('tekstLCD').addEventListener('input', function() {
document.getElementById('charCount').textContent = this.value.length;
});
// Aktualizacja wartości slidera
document.getElementById('predkoscSlider').addEventListener('input', function() {
document.getElementById('predkoscValue').textContent = this.value + 'ms';
});
// Wysłanie tekstu na LCD
function wyslijTekst() {
const tekst = document.getElementById('tekstLCD').value;
const predkosc = document.getElementById('predkoscSlider').value;
if (tekst.trim() === '') {
pokazStatus('statusTekst', 'Wpisz tekst!', 'warning');
return;
}
fetch('/wyslij-tekst?tekst=' + encodeURIComponent(tekst) + '&predkosc=' + predkosc)
.then(response => response.text())
.then(data => {
pokazStatus('statusTekst', '✅ Tekst wysłany na ekran!', 'success');
})
.catch(error => {
pokazStatus('statusTekst', '❌ Błąd wysyłania!', 'warning');
});
}
// Stop wyświetlania
function stopTekst() {
fetch('/stop-tekst')
.then(response => response.text())
.then(data => {
pokazStatus('statusTekst', '⏹️ Zatrzymano wyświetlanie', 'info');
});
}
// Wyczyść pole tekstowe
function wyczyscTekst() {
document.getElementById('tekstLCD').value = '';
document.getElementById('charCount').textContent = '0';
pokazStatus('statusTekst', '🗑️ Wyczyszczono', 'info');
}
// Dodaj gracza
function dodajGracza() {
const imie = document.getElementById('noweImie').value.trim();
if (imie === '' || imie.length > 20) {
alert('Podaj imię (1-20 znaków)!');
return;
}
fetch('/dodaj-gracza?imie=' + encodeURIComponent(imie))
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('noweImie').value = '';
pobierzGraczy();
} else {
alert(data.message);
}
});
}
// Usuń gracza
function usunGracza(index) {
if (confirm('Na pewno usunąć tego gracza?')) {
fetch('/usun-gracza?index=' + index)
.then(response => response.json())
.then(data => {
pobierzGraczy();
});
}
}
// Wybierz gracza
function wybierzGracza(index) {
fetch('/wybierz-gracza?index=' + index)
.then(response => response.json())
.then(data => {
pobierzGraczy();
odswiezStatus();
});
}
// Pobierz listę graczy
function pobierzGraczy() {
fetch('/gracze')
.then(response => response.json())
.then(data => {
const lista = document.getElementById('listaGraczy');
const tabela = document.getElementById('tabelaWynikow');
// Aktualizuj aktualnego gracza
if (data.gracze.length > 0 && data.aktualny >= 0) {
document.getElementById('aktualnyGraczNazwa').textContent =
data.gracze[data.aktualny].imie;
document.getElementById('aktualnyGraczWynik').textContent =
data.gracze[data.aktualny].wynik;
} else {
document.getElementById('aktualnyGraczNazwa').textContent = '---';
document.getElementById('aktualnyGraczWynik').textContent = '0';
}
// Lista graczy
lista.innerHTML = '';
data.gracze.forEach((gracz, index) => {
const isAktualny = index === data.aktualny;
lista.innerHTML += `
<div class="gracz-item" style="${isAktualny ? 'border: 2px solid #667eea;' : ''}">
<div class="gracz-info">
<div class="gracz-imie">${isAktualny ? '▶ ' : ''}${gracz.imie}</div>
<div class="gracz-wynik">Najlepszy wynik: ${gracz.wynik}</div>
</div>
<div>
<button class="btn-info btn-small" onclick="wybierzGracza(${index})">✓</button>
<button class="btn-danger btn-small" onclick="usunGracza(${index})">✕</button>
</div>
</div>
`;
});
if (data.gracze.length === 0) {
lista.innerHTML = '<p style="text-align:center;color:#666;">Brak graczy. Dodaj pierwszego!</p>';
}
// Tabela wyników (posortowana)
const posortowani = [...data.gracze].sort((a, b) => b.wynik - a.wynik);
tabela.innerHTML = '';
posortowani.forEach((gracz, index) => {
let medal = '';
if (index === 0 && gracz.wynik > 0) medal = '🥇';
else if (index === 1 && gracz.wynik > 0) medal = '🥈';
else if (index === 2 && gracz.wynik > 0) medal = '🥉';
tabela.innerHTML += `
<tr>
<td><span class="medal">${medal}</span> ${index + 1}</td>
<td>${gracz.imie}</td>
<td>${gracz.wynik} ⭐</td>
</tr>
`;
});
});
}
// Zapisz ustawienia
function zapiszUstawienia() {
const czasPokazu = document.getElementById('czasPokazu').value;
const czasPrzerwy = document.getElementById('czasPrzerwy').value;
const czasOczekiwania = document.getElementById('czasOczekiwania').value;
fetch('/zapisz-ustawienia?pokaz=' + czasPokazu +
'&przerwa=' + czasPrzerwy +
'&oczekiwanie=' + czasOczekiwania)
.then(response => response.json())
.then(data => {
pokazStatus('statusUstawienia', '✅ Zapisano ustawienia!', 'success');
});
}
// Pobierz ustawienia
function pobierzUstawienia() {
fetch('/ustawienia')
.then(response => response.json())
.then(data => {
document.getElementById('czasPokazu').value = data.pokaz;
document.getElementById('czasPrzerwy').value = data.przerwa;
document.getElementById('czasOczekiwania').value = data.oczekiwanie;
document.getElementById('predkoscSlider').value = data.przewijanie;
document.getElementById('predkoscValue').textContent = data.przewijanie + 'ms';
});
}
// Odśwież status
function odswiezStatus() {
fetch('/status')
.then(response => response.json())
.then(data => {
let statusHtml = '<strong>Stan:</strong> ' + (data.graAktywna ? '🎮 Gra w toku' : '⏸️ Oczekiwanie');
statusHtml += '<br><strong>Aktualny gracz:</strong> ' + data.gracz;
statusHtml += '<br><strong>Wyświetlanie tekstu:</strong> ' + (data.wyswietlanieTekstu ? '📺 Tak' : '❌ Nie');
document.getElementById('statusGry').innerHTML = statusHtml;
});
}
// Pokaż status
function pokazStatus(elementId, message, type) {
const element = document.getElementById(elementId);
element.textContent = message;
element.className = 'status-box status-' + type;
element.style.display = 'block';
setTimeout(() => {
element.style.display = 'none';
}, 3000);
}
// Inicjalizacja
document.addEventListener('DOMContentLoaded', function() {
pobierzGraczy();
pobierzUstawienia();
odswiezStatus();
// Auto-odświeżanie co 5 sekund
setInterval(() => {
pobierzGraczy();
odswiezStatus();
}, 5000);
});
</script>
</body>
</html>
)rawliteral";
// ============ FUNKCJE POMOCNICZE ============
int losowyKolor() {
uint32_t losowa = esp_random();
losowa ^= micros();
losowa ^= seedDodatkowy;
return losowa % 4;
}
void dodajEntropie() {
seedDodatkowy ^= esp_random();
seedDodatkowy ^= micros();
seedDodatkowy += millis();
}
void inicjalizujPiny() {
for (int i = 0; i < 4; i++) {
pinMode(ledy[i], OUTPUT);
digitalWrite(ledy[i], LOW);
}
for (int i = 0; i < 4; i++) {
pinMode(przyciski[i], INPUT_PULLUP);
}
pinMode(BUZZER, OUTPUT);
}
void zapalWszystkie() {
for (int i = 0; i < 4; i++) digitalWrite(ledy[i], HIGH);
}
void zgasWszystkie() {
for (int i = 0; i < 4; i++) digitalWrite(ledy[i], LOW);
}
void zapalLED(int numer, int czasMs) {
digitalWrite(ledy[numer], HIGH);
tone(BUZZER, tony[numer], czasMs);
delay(czasMs);
digitalWrite(ledy[numer], LOW);
noTone(BUZZER);
}
// ============ ZAPIS/ODCZYT DANYCH ============
void zapiszGraczy() {
preferences.begin("simon", false);
preferences.putInt("liczbaGraczy", liczbaGraczy);
preferences.putInt("ostatniGracz", ostatniGracz);
for (int i = 0; i < liczbaGraczy; i++) {
String keyImie = "imie" + String(i);
String keyWynik = "wynik" + String(i);
preferences.putString(keyImie.c_str(), gracze[i].imie);
preferences.putInt(keyWynik.c_str(), gracze[i].najlepszyWynik);
}
preferences.end();
}
void wczytajGraczy() {
preferences.begin("simon", true);
liczbaGraczy = preferences.getInt("liczbaGraczy", 0);
ostatniGracz = preferences.getInt("ostatniGracz", 0);
for (int i = 0; i < liczbaGraczy; i++) {
String keyImie = "imie" + String(i);
String keyWynik = "wynik" + String(i);
String imie = preferences.getString(keyImie.c_str(), "");
imie.toCharArray(gracze[i].imie, MAX_IMIE_LEN + 1);
gracze[i].najlepszyWynik = preferences.getInt(keyWynik.c_str(), 0);
}
preferences.end();
if (liczbaGraczy > 0 && ostatniGracz < liczbaGraczy) {
aktualnyGracz = ostatniGracz;
} else {
aktualnyGracz = 0;
}
}
void zapiszUstawienia() {
preferences.begin("simon-set", false);
preferences.putInt("pokaz", czasPokazu);
preferences.putInt("przerwa", czasPrzerwy);
preferences.putInt("oczekiwanie", czasOczekiwania);
preferences.putInt("przewijanie", predkoscPrzewijania);
preferences.end();
}
void wczytajUstawienia() {
preferences.begin("simon-set", true);
czasPokazu = preferences.getInt("pokaz", 600);
czasPrzerwy = preferences.getInt("przerwa", 300);
czasOczekiwania = preferences.getInt("oczekiwanie", 5000);
predkoscPrzewijania = preferences.getInt("przewijanie", 300);
preferences.end();
}
// ============ OBSŁUGA WWW ============
void handleRoot() {
server.send(200, "text/html", STRONA_HTML);
}
void handleGracze() {
String json = "{\"gracze\":[";
for (int i = 0; i < liczbaGraczy; i++) {
if (i > 0) json += ",";
json += "{\"imie\":\"" + String(gracze[i].imie) + "\",\"wynik\":" + String(gracze[i].najlepszyWynik) + "}";
}
json += "],\"aktualny\":" + String(aktualnyGracz) + "}";
server.send(200, "application/json", json);
}
void handleDodajGracza() {
if (liczbaGraczy >= MAX_GRACZY) {
server.send(200, "application/json", "{\"success\":false,\"message\":\"Maksymalna liczba graczy!\"}");
return;
}
String imie = server.arg("imie");
imie.trim();
if (imie.length() == 0 || imie.length() > MAX_IMIE_LEN) {
server.send(200, "application/json", "{\"success\":false,\"message\":\"Nieprawidłowe imię!\"}");
return;
}
imie.toCharArray(gracze[liczbaGraczy].imie, MAX_IMIE_LEN + 1);
gracze[liczbaGraczy].najlepszyWynik = 0;
liczbaGraczy++;
zapiszGraczy();
server.send(200, "application/json", "{\"success\":true}");
}
void handleUsunGracza() {
int index = server.arg("index").toInt();
if (index >= 0 && index < liczbaGraczy) {
for (int i = index; i < liczbaGraczy - 1; i++) {
strcpy(gracze[i].imie, gracze[i + 1].imie);
gracze[i].najlepszyWynik = gracze[i + 1].najlepszyWynik;
}
liczbaGraczy--;
if (aktualnyGracz >= liczbaGraczy) {
aktualnyGracz = max(0, liczbaGraczy - 1);
}
zapiszGraczy();
}
server.send(200, "application/json", "{\"success\":true}");
}
void handleWybierzGracza() {
int index = server.arg("index").toInt();
if (index >= 0 && index < liczbaGraczy) {
aktualnyGracz = index;
ostatniGracz = index;
zapiszGraczy();
}
server.send(200, "application/json", "{\"success\":true}");
}
void handleWyslijTekst() {
String tekst = server.arg("tekst");
int predkosc = server.arg("predkosc").toInt();
tekst.toCharArray(tekstDoWyswietlenia, MAX_TEXT_LEN + 1);
predkoscPrzewijania = constrain(predkosc, 100, 800);
wyswietlanieTekstu = true;
czasStartuTekstu = millis();
pozycjaPrzewijania = 16; // Zaczynamy od prawej strony
zapiszUstawienia();
server.send(200, "text/plain", "OK");
}
void handleStopTekst() {
wyswietlanieTekstu = false;
tekstDoWyswietlenia[0] = '\0';
server.send(200, "text/plain", "OK");
}
void handleZapiszUstawienia() {
czasPokazu = constrain(server.arg("pokaz").toInt(), 200, 2000);
czasPrzerwy = constrain(server.arg("przerwa").toInt(), 100, 1000);
czasOczekiwania = constrain(server.arg("oczekiwanie").toInt(), 2000, 15000);
zapiszUstawienia();
server.send(200, "application/json", "{\"success\":true}");
}
void handlePobierzUstawienia() {
String json = "{\"pokaz\":" + String(czasPokazu) +
",\"przerwa\":" + String(czasPrzerwy) +
",\"oczekiwanie\":" + String(czasOczekiwania) +
",\"przewijanie\":" + String(predkoscPrzewijania) + "}";
server.send(200, "application/json", json);
}
void handleStatus() {
String graczNazwa = (liczbaGraczy > 0) ? String(gracze[aktualnyGracz].imie) : "Brak";
String json = "{\"graAktywna\":" + String(graAktywna ? "true" : "false") +
",\"gracz\":\"" + graczNazwa + "\"" +
",\"wyswietlanieTekstu\":" + String(wyswietlanieTekstu ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
void setupServer() {
server.on("/", handleRoot);
server.on("/gracze", handleGracze);
server.on("/dodaj-gracza", handleDodajGracza);
server.on("/usun-gracza", handleUsunGracza);
server.on("/wybierz-gracza", handleWybierzGracza);
server.on("/wyslij-tekst", handleWyslijTekst);
server.on("/stop-tekst", handleStopTekst);
server.on("/zapisz-ustawienia", handleZapiszUstawienia);
server.on("/ustawienia", handlePobierzUstawienia);
server.on("/status", handleStatus);
server.begin();
}
// ============ WYŚWIETLANIE TEKSTU ============
void obslugaWyswietlaniaTekstu() {
if (!wyswietlanieTekstu) return;
// Sprawdź timeout (1 minuta)
if (millis() - czasStartuTekstu > 60000) {
wyswietlanieTekstu = false;
return;
}
int dlugoscTekstu = strlen(tekstDoWyswietlenia);
// Krótki tekst - wyświetl statycznie
if (dlugoscTekstu <= 16) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(tekstDoWyswietlenia);
delay(500); // Odśwież co 500ms
return;
}
// Długi tekst - przewijaj
if (millis() - ostatniePrzewijanie >= predkoscPrzewijania) {
ostatniePrzewijanie = millis();
lcd.clear();
lcd.setCursor(0, 0);
// Wyświetl fragment tekstu
for (int i = 0; i < 16; i++) {
int pos = pozycjaPrzewijania + i;
if (pos >= 0 && pos < dlugoscTekstu) {
lcd.print(tekstDoWyswietlenia[pos]);
} else if (pos >= dlugoscTekstu && pos < dlugoscTekstu + 16) {
lcd.print(' ');
} else if (pos >= dlugoscTekstu + 16) {
// Reset po przerwie
int wrapPos = pos - dlugoscTekstu - 16;
if (wrapPos < dlugoscTekstu) {
lcd.print(tekstDoWyswietlenia[wrapPos]);
}
}
}
pozycjaPrzewijania--;
// Reset przewijania
if (pozycjaPrzewijania < -dlugoscTekstu - 16) {
pozycjaPrzewijania = 16;
}
}
}
// ============ EKRAN WYBORU GRACZA ============
void wyswietlWyborGracza() {
lcd.clear();
if (liczbaGraczy == 0) {
lcd.setCursor(0, 0);
lcd.print("Dodaj gracza");
lcd.setCursor(0, 1);
lcd.print("przez WiFi!");
return;
}
// Górna linia: imię gracza
lcd.setCursor(0, 0);
lcd.write(4); // Strzałka lewa
lcd.print(" ");
String imie = String(gracze[aktualnyGracz].imie);
int padding = (12 - imie.length()) / 2;
for (int i = 0; i < padding; i++) lcd.print(" ");
lcd.print(imie);
for (int i = 0; i < padding; i++) lcd.print(" ");
lcd.print(" ");
lcd.write(5); // Strzałka prawa
// Dolna linia: instrukcja + wynik
lcd.setCursor(0, 1);
lcd.print("START ");
lcd.write(2); // Gwiazdka
lcd.print(" W:");
lcd.print(gracze[aktualnyGracz].najlepszyWynik);
}
bool ekranWyboruGracza() {
wyswietlWyborGracza();
while (true) {
server.handleClient();
// Jeśli wyświetlamy tekst z WWW, przerwij wybór
if (wyswietlanieTekstu) {
while (wyswietlanieTekstu) {
obslugaWyswietlaniaTekstu();
server.handleClient();
// Sprawdź czy wciśnięto przycisk STOP (dowolny przycisk)
for (int i = 0; i < 4; i++) {
if (digitalRead(przyciski[i]) == LOW) {
wyswietlanieTekstu = false;
delay(300);
break;
}
}
}
wyswietlWyborGracza();
}
// Przycisk CZERWONY - poprzedni gracz
if (digitalRead(BTN_CZERWONY) == LOW) {
delay(200);
if (liczbaGraczy > 0) {
aktualnyGracz = (aktualnyGracz - 1 + liczbaGraczy) % liczbaGraczy;
ostatniGracz = aktualnyGracz;
zapiszGraczy();
wyswietlWyborGracza();
zapalLED(0, 100);
}
while (digitalRead(BTN_CZERWONY) == LOW) delay(10);
}
// Przycisk ZIELONY - następny gracz
if (digitalRead(BTN_ZIELONY) == LOW) {
delay(200);
if (liczbaGraczy > 0) {
aktualnyGracz = (aktualnyGracz + 1) % liczbaGraczy;
ostatniGracz = aktualnyGracz;
zapiszGraczy();
wyswietlWyborGracza();
zapalLED(1, 100);
}
while (digitalRead(BTN_ZIELONY) == LOW) delay(10);
}
// Przycisk NIEBIESKI lub ŻÓŁTY - start gry
if (digitalRead(BTN_NIEBIESKI) == LOW || digitalRead(BTN_ZOLTY) == LOW) {
delay(200);
if (liczbaGraczy > 0) {
zapalLED(2, 100);
zapalLED(3, 100);
return true; // Rozpocznij grę
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Najpierw dodaj");
lcd.setCursor(0, 1);
lcd.print("gracza w WiFi!");
delay(2000);
wyswietlWyborGracza();
}
while (digitalRead(BTN_NIEBIESKI) == LOW || digitalRead(BTN_ZOLTY) == LOW) delay(10);
}
delay(50);
}
}
// ============ FUNKCJE GRY ============
void generujNowaSekwencje() {
for (int i = 0; i < 10; i++) {
dodajEntropie();
delay(10);
}
for (int i = 0; i < MAX_POZIOM; i++) {
sekwencja[i] = losowyKolor();
}
Serial.println("Nowa sekwencja wygenerowana");
}
void pokazSekwencje() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" PATRZ! ");
lcd.write(3);
lcd.setCursor(0, 1);
lcd.print(" Zapamietaj!");
delay(800);
for (int i = 0; i < poziom; i++) {
server.handleClient();
lcd.setCursor(0, 1);
lcd.print("Kolor ");
lcd.print(i + 1);
lcd.print(" z ");
lcd.print(poziom);
lcd.print(" ");
zapalLED(sekwencja[i], czasPokazu);
delay(czasPrzerwy);
}
}
int sprawdzPrzycisk() {
for (int i = 0; i < 4; i++) {
if (digitalRead(przyciski[i]) == LOW) {
delay(50);
if (digitalRead(przyciski[i]) == LOW) {
seedDodatkowy ^= micros();
zapalLED(i, 200);
while (digitalRead(przyciski[i]) == LOW) delay(10);
delay(100);
return i;
}
}
}
return -1;
}
bool czekajNaOdpowiedz() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Twoja kolej! ");
lcd.write(0);
int krokGracza = 0;
while (krokGracza < poziom) {
lcd.setCursor(0, 1);
lcd.print("Kolor ");
lcd.print(krokGracza + 1);
lcd.print(" z ");
lcd.print(poziom);
lcd.print(" ");
unsigned long czasStart = millis();
int nacisniety = -1;
while (nacisniety == -1) {
server.handleClient();
nacisniety = sprawdzPrzycisk();
if (millis() - czasStart > czasOczekiwania) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Za wolno! ");
lcd.write(1);
delay(1500);
return false;
}
// Migająca gwiazdka
lcd.setCursor(15, 0);
lcd.print((millis() / 500) % 2 == 0 ? "*" : " ");
}
if (nacisniety != sekwencja[krokGracza]) {
return false;
}
krokGracza++;
}
return true;
}
void efektSukces() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" BRAWO!!! ");
lcd.write(0);
lcd.setCursor(0, 1);
lcd.print(" SUPER ROBOTA!");
for (int i = 0; i < 3; i++) {
zapalWszystkie();
tone(BUZZER, TON_SUKCES, 150);
delay(200);
zgasWszystkie();
noTone(BUZZER);
delay(100);
}
delay(1000);
}
void efektPorazka() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Ups! ");
lcd.write(1);
lcd.setCursor(0, 1);
lcd.print("Sprobuj jeszcze!");
tone(BUZZER, TON_PORAZKA, 500);
for (int i = 0; i < 3; i++) {
zapalWszystkie();
delay(150);
zgasWszystkie();
delay(150);
}
noTone(BUZZER);
delay(1500);
}
void pokazWynikKoncowy(int wynik) {
// Aktualizuj rekord gracza
if (wynik > gracze[aktualnyGracz].najlepszyWynik) {
gracze[aktualnyGracz].najlepszyWynik = wynik;
zapiszGraczy();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("NOWY REKORD!");
lcd.setCursor(0, 1);
lcd.print(gracze[aktualnyGracz].imie);
lcd.print(": ");
lcd.print(wynik);
lcd.print(" ");
lcd.write(2);
// Efekt świetlny
for (int i = 0; i < 5; i++) {
zapalWszystkie();
tone(BUZZER, TON_SUKCES + i * 100, 100);
delay(150);
zgasWszystkie();
delay(100);
}
noTone(BUZZER);
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Wynik: ");
lcd.print(wynik);
lcd.setCursor(0, 1);
lcd.print("Rekord: ");
lcd.print(gracze[aktualnyGracz].najlepszyWynik);
}
delay(3000);
}
void rozgrywka() {
poziom = 0;
graAktywna = true;
generujNowaSekwencje();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.print(gracze[aktualnyGracz].imie);
lcd.setCursor(0, 1);
lcd.print(" Zaczynamy!");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" 3... 2... 1...");
delay(1500);
while (graAktywna && poziom < MAX_POZIOM) {
poziom++;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Poziom: ");
lcd.print(poziom);
lcd.print(" ");
lcd.write(2);
delay(1000);
pokazSekwencje();
delay(500);
if (czekajNaOdpowiedz()) {
efektSukces();
delay(500);
} else {
efektPorazka();
graAktywna = false;
}
}
int wynikKoncowy = (poziom > 0) ? poziom - 1 : 0;
if (graAktywna) wynikKoncowy = poziom; // Ukończył wszystkie poziomy!
pokazWynikKoncowy(wynikKoncowy);
graAktywna = false;
}
// ============ SETUP I LOOP ============
void setup() {
Serial.begin(115200);
Serial.println("\n\n=== SIMON SAYS - WERSJA ROZBUDOWANA ===");
// Inicjalizacja
seedDodatkowy = esp_random();
for (int i = 0; i < 20; i++) {
dodajEntropie();
delay(5);
}
inicjalizujPiny();
// LCD
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
lcd.createChar(0, buzkaUsmiech);
lcd.createChar(1, buzkaSmutek);
lcd.createChar(2, gwiazdka);
lcd.createChar(3, serce);
lcd.createChar(4, strzalkaL);
lcd.createChar(5, strzalkaP);
// Ekran startowy
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Simon Says");
lcd.setCursor(0, 1);
lcd.print(" Ladowanie...");
// Wczytaj dane
wczytajGraczy();
wczytajUstawienia();
// WiFi Access Point
WiFi.softAP(AP_SSID, AP_PASS);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP SSID: ");
Serial.println(AP_SSID);
Serial.print("IP: ");
Serial.println(IP);
// Serwer WWW
setupServer();
// Pokaż info o WiFi
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi: ");
lcd.print(AP_SSID);
lcd.setCursor(0, 1);
lcd.print("IP: ");
lcd.print(IP);
delay(3000);
// Animacja powitalna
for (int j = 0; j < 2; j++) {
for (int i = 0; i < 4; i++) {
zapalLED(i, 100);
delay(50);
}
}
}
void loop() {
server.handleClient();
// Obsłuż wyświetlanie tekstu z WWW
if (wyswietlanieTekstu) {
obslugaWyswietlaniaTekstu();
return;
}
// Ekran wyboru gracza
if (ekranWyboruGracza()) {
// Gra!
rozgrywka();
}
}