492 lines
17 KiB
C++
492 lines
17 KiB
C++
#include "Game.h"
|
|
#include <vector>
|
|
#include <string>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <cmath>
|
|
|
|
Game::Game()
|
|
: snake(Point(BOARD_WIDTH / 2, BOARD_HEIGHT / 2)),
|
|
score(0),
|
|
bestScore(0),
|
|
gameOver(false),
|
|
paused(false),
|
|
inMenu(true)
|
|
{
|
|
loadBestScore();
|
|
generateFood();
|
|
}
|
|
|
|
void Game::run()
|
|
{
|
|
sf::RenderWindow window(sf::VideoMode(BOARD_WIDTH * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE + 60), "Snake Game");
|
|
sf::Clock clock;
|
|
float timer = 0.0f;
|
|
|
|
// Menu buttons initialization
|
|
MenuButton startBtn, exitBtn, restartBtn, menuBtn;
|
|
startBtn.rect = sf::FloatRect(CELL_SIZE * BOARD_WIDTH / 2 - 110, 180, 220, 48);
|
|
startBtn.label = "Start";
|
|
startBtn.color = sf::Color(80, 180, 80);
|
|
startBtn.hoverColor = sf::Color(120, 220, 120);
|
|
|
|
exitBtn.rect = sf::FloatRect(BOARD_WIDTH * CELL_SIZE / 2 - 110, 250, 220, 40);
|
|
exitBtn.label = "Exit";
|
|
exitBtn.color = sf::Color(180, 80, 80);
|
|
exitBtn.hoverColor = sf::Color(220, 120, 120);
|
|
|
|
restartBtn.rect = sf::FloatRect(BOARD_WIDTH * CELL_SIZE / 2 - 110, 180, 220, 48);
|
|
restartBtn.label = "Restart";
|
|
restartBtn.color = sf::Color(80, 180, 80);
|
|
restartBtn.hoverColor = sf::Color(120, 220, 120);
|
|
|
|
menuBtn.rect = sf::FloatRect(BOARD_WIDTH * CELL_SIZE / 2 - 110, 250, 220, 40);
|
|
menuBtn.label = "Menu";
|
|
menuBtn.color = sf::Color(180, 80, 80);
|
|
menuBtn.hoverColor = sf::Color(220, 120, 120);
|
|
|
|
while (window.isOpen())
|
|
{
|
|
sf::Event event;
|
|
while (window.pollEvent(event))
|
|
{
|
|
if (event.type == sf::Event::Closed)
|
|
window.close();
|
|
processEvent(event, &window, &startBtn, &exitBtn, &restartBtn, &menuBtn);
|
|
}
|
|
|
|
if (!paused && !gameOver && !inMenu)
|
|
{
|
|
float delay = 0.15f;
|
|
timer += clock.restart().asSeconds();
|
|
if (timer > delay)
|
|
{
|
|
update();
|
|
timer = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
clock.restart();
|
|
}
|
|
|
|
render(window, &startBtn, &exitBtn, &restartBtn, &menuBtn);
|
|
}
|
|
}
|
|
|
|
// Handles all events, including mouse support for menu buttons
|
|
void Game::processEvent(const sf::Event& event, sf::RenderWindow* window,
|
|
MenuButton* startBtn, MenuButton* exitBtn,
|
|
MenuButton* restartBtn, MenuButton* menuBtn)
|
|
{
|
|
sf::Vector2f mousePos;
|
|
if (window)
|
|
mousePos = window->mapPixelToCoords(sf::Mouse::getPosition(*window));
|
|
|
|
// Reset hover state for all buttons
|
|
if (startBtn) startBtn->hovered = false;
|
|
if (exitBtn) exitBtn->hovered = false;
|
|
if (restartBtn) restartBtn->hovered = false;
|
|
if (menuBtn) menuBtn->hovered = false;
|
|
|
|
if (inMenu)
|
|
{
|
|
if (startBtn && startBtn->rect.contains(mousePos))
|
|
startBtn->hovered = true;
|
|
if (exitBtn && exitBtn->rect.contains(mousePos))
|
|
exitBtn->hovered = true;
|
|
|
|
if (event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left)
|
|
{
|
|
if (startBtn && startBtn->rect.contains(mousePos))
|
|
{
|
|
inMenu = false;
|
|
gameOver = false;
|
|
paused = false;
|
|
reset();
|
|
return;
|
|
}
|
|
if (exitBtn && exitBtn->rect.contains(mousePos))
|
|
{
|
|
if (window) window->close();
|
|
return;
|
|
}
|
|
}
|
|
if (event.type == sf::Event::KeyPressed)
|
|
{
|
|
if (event.key.code == sf::Keyboard::Enter)
|
|
{
|
|
inMenu = false;
|
|
gameOver = false;
|
|
paused = false;
|
|
reset();
|
|
}
|
|
else if (event.key.code == sf::Keyboard::Escape)
|
|
{
|
|
if (window) window->close();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (gameOver)
|
|
{
|
|
if (restartBtn && restartBtn->rect.contains(mousePos))
|
|
restartBtn->hovered = true;
|
|
if (menuBtn && menuBtn->rect.contains(mousePos))
|
|
menuBtn->hovered = true;
|
|
|
|
if (event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left)
|
|
{
|
|
if (restartBtn && restartBtn->rect.contains(mousePos))
|
|
{
|
|
reset();
|
|
gameOver = false;
|
|
paused = false;
|
|
inMenu = false;
|
|
return;
|
|
}
|
|
if (menuBtn && menuBtn->rect.contains(mousePos))
|
|
{
|
|
inMenu = true;
|
|
return;
|
|
}
|
|
}
|
|
if (event.type == sf::Event::KeyPressed)
|
|
{
|
|
if (event.key.code == sf::Keyboard::Enter)
|
|
{
|
|
reset();
|
|
gameOver = false;
|
|
paused = false;
|
|
inMenu = false;
|
|
}
|
|
else if (event.key.code == sf::Keyboard::Escape)
|
|
{
|
|
inMenu = true;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Snake controls
|
|
if (event.type == sf::Event::KeyPressed)
|
|
{
|
|
switch (event.key.code)
|
|
{
|
|
case sf::Keyboard::W:
|
|
case sf::Keyboard::Up:
|
|
if (snake.getDirection() != Direction::DOWN)
|
|
snake.setDirection(Direction::UP);
|
|
paused = false;
|
|
break;
|
|
case sf::Keyboard::S:
|
|
case sf::Keyboard::Down:
|
|
if (snake.getDirection() != Direction::UP)
|
|
snake.setDirection(Direction::DOWN);
|
|
paused = false;
|
|
break;
|
|
case sf::Keyboard::A:
|
|
case sf::Keyboard::Left:
|
|
if (snake.getDirection() != Direction::RIGHT)
|
|
snake.setDirection(Direction::LEFT);
|
|
paused = false;
|
|
break;
|
|
case sf::Keyboard::D:
|
|
case sf::Keyboard::Right:
|
|
if (snake.getDirection() != Direction::LEFT)
|
|
snake.setDirection(Direction::RIGHT);
|
|
paused = false;
|
|
break;
|
|
case sf::Keyboard::Space:
|
|
paused = !paused;
|
|
break;
|
|
case sf::Keyboard::Q:
|
|
case sf::Keyboard::Escape:
|
|
inMenu = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rendering with interactive menu buttons and centered text
|
|
void Game::render(sf::RenderWindow& window,
|
|
MenuButton* startBtn, MenuButton* exitBtn,
|
|
MenuButton* restartBtn, MenuButton* menuBtn) const {
|
|
// Draw gradient background
|
|
sf::VertexArray background(sf::Quads, 4);
|
|
background[0].position = sf::Vector2f(0, 0);
|
|
background[1].position = sf::Vector2f(window.getSize().x, 0);
|
|
background[2].position = sf::Vector2f(window.getSize().x, window.getSize().y);
|
|
background[3].position = sf::Vector2f(0, window.getSize().y);
|
|
background[0].color = sf::Color(40, 40, 60);
|
|
background[1].color = sf::Color(40, 40, 60);
|
|
background[2].color = sf::Color(20, 20, 30);
|
|
background[3].color = sf::Color(20, 20, 30);
|
|
window.draw(background);
|
|
|
|
// Draw game border
|
|
sf::RectangleShape border(sf::Vector2f(BOARD_WIDTH * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE));
|
|
border.setPosition(0, 0);
|
|
border.setFillColor(sf::Color::Transparent);
|
|
border.setOutlineThickness(3);
|
|
border.setOutlineColor(sf::Color(180, 180, 220));
|
|
window.draw(border);
|
|
|
|
// Draw food with highlight
|
|
sf::CircleShape foodCircle(CELL_SIZE / 2 - 3);
|
|
foodCircle.setFillColor(sf::Color(255, 80, 80));
|
|
foodCircle.setOutlineThickness(2);
|
|
foodCircle.setOutlineColor(sf::Color(255, 180, 180));
|
|
foodCircle.setOrigin(foodCircle.getRadius(), foodCircle.getRadius());
|
|
foodCircle.setPosition(food.getPosition().x * CELL_SIZE + CELL_SIZE / 2, food.getPosition().y * CELL_SIZE + CELL_SIZE / 2);
|
|
window.draw(foodCircle);
|
|
|
|
// Draw snake
|
|
for (const auto& segment : snake.getBody())
|
|
{
|
|
sf::RectangleShape cell(sf::Vector2f(CELL_SIZE - 4, CELL_SIZE - 4));
|
|
cell.setPosition(segment.x * CELL_SIZE + 2, segment.y * CELL_SIZE + 2);
|
|
if (segment == snake.getHead()) {
|
|
cell.setFillColor(sf::Color(80, 255, 80));
|
|
cell.setOutlineThickness(2);
|
|
cell.setOutlineColor(sf::Color(40, 180, 40));
|
|
} else {
|
|
cell.setFillColor(sf::Color(100, 200, 100));
|
|
cell.setOutlineThickness(1);
|
|
cell.setOutlineColor(sf::Color(60, 120, 60));
|
|
}
|
|
window.draw(cell);
|
|
}
|
|
|
|
// Helper for drawing shadowed text
|
|
auto drawShadowedText = [&](sf::RenderTarget& target, const sf::Text& text, sf::Color shadowColor = sf::Color(0,0,0,120), float offset = 2.f) {
|
|
sf::Text shadow = text;
|
|
shadow.setFillColor(shadowColor);
|
|
shadow.move(offset, offset);
|
|
target.draw(shadow);
|
|
target.draw(text);
|
|
};
|
|
|
|
static sf::Font font;
|
|
static bool fontLoaded = false;
|
|
if (!fontLoaded) {
|
|
fontLoaded = font.loadFromFile("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf");
|
|
}
|
|
|
|
if (fontLoaded)
|
|
{
|
|
sf::Text text;
|
|
text.setFont(font);
|
|
text.setCharacterSize(22);
|
|
text.setFillColor(sf::Color::White);
|
|
|
|
if (inMenu)
|
|
{
|
|
// Title
|
|
text.setString("SNAKE GAME");
|
|
text.setCharacterSize(38);
|
|
text.setStyle(sf::Text::Bold);
|
|
sf::FloatRect titleRect = text.getLocalBounds();
|
|
text.setOrigin(titleRect.left + titleRect.width / 2.0f, titleRect.top + titleRect.height / 2.0f);
|
|
text.setPosition(BOARD_WIDTH * CELL_SIZE / 2.0f, 80);
|
|
drawShadowedText(window, text);
|
|
|
|
// Best score
|
|
text.setString("Best score: " + std::to_string(bestScore));
|
|
text.setCharacterSize(22);
|
|
text.setStyle(sf::Text::Regular);
|
|
sf::FloatRect bestRect = text.getLocalBounds();
|
|
text.setOrigin(bestRect.left + bestRect.width / 2.0f, bestRect.top + bestRect.height / 2.0f);
|
|
text.setPosition(BOARD_WIDTH * CELL_SIZE / 2.0f, 130);
|
|
drawShadowedText(window, text);
|
|
|
|
// Start button
|
|
sf::RectangleShape startShape(sf::Vector2f(startBtn->rect.width, startBtn->rect.height));
|
|
startShape.setPosition(startBtn->rect.left, startBtn->rect.top);
|
|
startShape.setFillColor(startBtn->hovered ? startBtn->hoverColor : startBtn->color);
|
|
startShape.setOutlineThickness(3);
|
|
startShape.setOutlineColor(sf::Color(40, 120, 40));
|
|
window.draw(startShape);
|
|
|
|
text.setString("Start");
|
|
text.setCharacterSize(24);
|
|
text.setFillColor(sf::Color::White);
|
|
sf::FloatRect startTextRect = text.getLocalBounds();
|
|
text.setOrigin(startTextRect.left + startTextRect.width / 2.0f, startTextRect.top + startTextRect.height / 2.0f);
|
|
text.setPosition(
|
|
startBtn->rect.left + startBtn->rect.width / 2.0f,
|
|
startBtn->rect.top + startBtn->rect.height / 2.0f
|
|
);
|
|
drawShadowedText(window, text);
|
|
|
|
// Exit button
|
|
sf::RectangleShape exitShape(sf::Vector2f(exitBtn->rect.width, exitBtn->rect.height));
|
|
exitShape.setPosition(exitBtn->rect.left, exitBtn->rect.top);
|
|
exitShape.setFillColor(exitBtn->hovered ? exitBtn->hoverColor : exitBtn->color);
|
|
exitShape.setOutlineThickness(3);
|
|
exitShape.setOutlineColor(sf::Color(120, 40, 40));
|
|
window.draw(exitShape);
|
|
|
|
text.setString("Exit");
|
|
text.setCharacterSize(22);
|
|
text.setFillColor(sf::Color::White);
|
|
sf::FloatRect exitTextRect = text.getLocalBounds();
|
|
text.setOrigin(exitTextRect.left + exitTextRect.width / 2.0f, exitTextRect.top + exitTextRect.height / 2.0f);
|
|
text.setPosition(
|
|
exitBtn->rect.left + exitBtn->rect.width / 2.0f,
|
|
exitBtn->rect.top + exitBtn->rect.height / 2.0f
|
|
);
|
|
drawShadowedText(window, text);
|
|
}
|
|
else if (gameOver)
|
|
{
|
|
// Game Over title
|
|
text.setString("GAME OVER");
|
|
text.setCharacterSize(38);
|
|
text.setStyle(sf::Text::Bold);
|
|
text.setFillColor(sf::Color(255, 80, 80));
|
|
sf::FloatRect overRect = text.getLocalBounds();
|
|
text.setOrigin(overRect.left + overRect.width / 2.0f, overRect.top + overRect.height / 2.0f);
|
|
text.setPosition(BOARD_WIDTH * CELL_SIZE / 2.0f, 80);
|
|
drawShadowedText(window, text, sf::Color(0,0,0,180), 3.f);
|
|
|
|
// Score info
|
|
text.setString("Score: " + std::to_string(score) + " Best: " + std::to_string(bestScore));
|
|
text.setCharacterSize(22);
|
|
text.setStyle(sf::Text::Regular);
|
|
text.setFillColor(sf::Color::White);
|
|
sf::FloatRect scoreRect = text.getLocalBounds();
|
|
text.setOrigin(scoreRect.left + scoreRect.width / 2.0f, scoreRect.top + scoreRect.height / 2.0f);
|
|
text.setPosition(BOARD_WIDTH * CELL_SIZE / 2.0f, 130);
|
|
drawShadowedText(window, text);
|
|
|
|
// Restart button
|
|
sf::RectangleShape restartShape(sf::Vector2f(restartBtn->rect.width, restartBtn->rect.height));
|
|
restartShape.setPosition(restartBtn->rect.left, restartBtn->rect.top);
|
|
restartShape.setFillColor(restartBtn->hovered ? restartBtn->hoverColor : restartBtn->color);
|
|
restartShape.setOutlineThickness(3);
|
|
restartShape.setOutlineColor(sf::Color(40, 120, 40));
|
|
window.draw(restartShape);
|
|
|
|
text.setString("Restart");
|
|
text.setCharacterSize(24);
|
|
text.setFillColor(sf::Color::White);
|
|
sf::FloatRect restartTextRect = text.getLocalBounds();
|
|
text.setOrigin(restartTextRect.left + restartTextRect.width / 2.0f, restartTextRect.top + restartTextRect.height / 2.0f);
|
|
text.setPosition(
|
|
restartBtn->rect.left + restartBtn->rect.width / 2.0f,
|
|
restartBtn->rect.top + restartBtn->rect.height / 2.0f
|
|
);
|
|
drawShadowedText(window, text);
|
|
|
|
// Menu button
|
|
sf::RectangleShape menuShape(sf::Vector2f(menuBtn->rect.width, menuBtn->rect.height));
|
|
menuShape.setPosition(menuBtn->rect.left, menuBtn->rect.top);
|
|
menuShape.setFillColor(menuBtn->hovered ? menuBtn->hoverColor : menuBtn->color);
|
|
menuShape.setOutlineThickness(3);
|
|
menuShape.setOutlineColor(sf::Color(120, 40, 40));
|
|
window.draw(menuShape);
|
|
|
|
text.setString("Menu");
|
|
text.setCharacterSize(22);
|
|
text.setFillColor(sf::Color::White);
|
|
sf::FloatRect menuTextRect = text.getLocalBounds();
|
|
text.setOrigin(menuTextRect.left + menuTextRect.width / 2.0f, menuTextRect.top + menuTextRect.height / 2.0f);
|
|
text.setPosition(
|
|
menuBtn->rect.left + menuBtn->rect.width / 2.0f,
|
|
menuBtn->rect.top + menuBtn->rect.height / 2.0f
|
|
);
|
|
drawShadowedText(window, text);
|
|
}
|
|
else
|
|
{
|
|
// Score bar
|
|
text.setString("Score: " + std::to_string(score) + " Best: " + std::to_string(bestScore) +
|
|
(paused ? " [PAUSED]" : ""));
|
|
text.setCharacterSize(22);
|
|
text.setFillColor(sf::Color::White);
|
|
text.setStyle(sf::Text::Regular);
|
|
text.setPosition(5, BOARD_HEIGHT * CELL_SIZE + 8);
|
|
drawShadowedText(window, text);
|
|
|
|
// Controls
|
|
text.setString("WASD / Arrows - Move SPACE - Pause ESC - Menu");
|
|
text.setCharacterSize(16);
|
|
text.setFillColor(sf::Color(180, 180, 220));
|
|
text.setPosition(5, BOARD_HEIGHT * CELL_SIZE + 36);
|
|
drawShadowedText(window, text, sf::Color(0,0,0,100), 1.5f);
|
|
}
|
|
}
|
|
|
|
window.display();
|
|
}
|
|
|
|
void Game::update()
|
|
{
|
|
snake.updateDirection();
|
|
Point newHead = getTeleportedHead();
|
|
|
|
if (snake.checkSelfCollision(newHead))
|
|
{
|
|
gameOver = true;
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
saveBestScore();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (newHead == food.getPosition())
|
|
{
|
|
snake.grow(newHead);
|
|
score += 10;
|
|
generateFood();
|
|
}
|
|
else
|
|
{
|
|
snake.move(newHead);
|
|
}
|
|
}
|
|
|
|
Point Game::getTeleportedHead() const
|
|
{
|
|
Point newHead = snake.getNextHeadPosition();
|
|
if (newHead.x < 0) newHead.x = BOARD_WIDTH - 1;
|
|
else if (newHead.x >= BOARD_WIDTH) newHead.x = 0;
|
|
if (newHead.y < 0) newHead.y = BOARD_HEIGHT - 1;
|
|
else if (newHead.y >= BOARD_HEIGHT) newHead.y = 0;
|
|
return newHead;
|
|
}
|
|
|
|
void Game::generateFood()
|
|
{
|
|
std::vector<Point> occupiedPositions(snake.getBody().begin(), snake.getBody().end());
|
|
food.generateNew(BOARD_WIDTH, BOARD_HEIGHT, occupiedPositions);
|
|
}
|
|
|
|
void Game::loadBestScore()
|
|
{
|
|
std::ifstream in(BEST_SCORE_FILE);
|
|
if (in)
|
|
in >> bestScore;
|
|
else
|
|
bestScore = 0;
|
|
}
|
|
|
|
void Game::saveBestScore() {
|
|
std::ofstream out(BEST_SCORE_FILE);
|
|
if (out)
|
|
out << bestScore;
|
|
}
|
|
|
|
void Game::reset()
|
|
{
|
|
snake = Snake(Point(BOARD_WIDTH / 2, BOARD_HEIGHT / 2));
|
|
score = 0;
|
|
generateFood();
|
|
}
|