Files
cppsnek/main.cpp

672 lines
21 KiB
C++

#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <vector>
#include <random>
#include <array>
// Note: Make sure to link with sfml-audio in your CMakeLists.txt
// add_executable(cppsnek main.cpp)
// target_link_libraries(cppsnek sfml-graphics sfml-window sfml-system sfml-audio)
// Set namespaces for SFML and STD
using namespace sf;
using namespace std;
// Init window / sprite variables ( constant and will never change )
constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
constexpr int GRID_SIZE = 20;
constexpr int GRID_WIDTH = WIDTH / GRID_SIZE;
constexpr int GRID_HEIGHT = HEIGHT / GRID_SIZE;
constexpr int SPRITE_SIZE = GRID_SIZE;
struct SnakeSegment {
int x, y;
};
class NumericSprite {
private:
Texture texture;
Sprite sprite;
public:
NumericSprite() {
texture.create(SPRITE_SIZE, SPRITE_SIZE);
sprite.setTexture(texture);
}
void createFromPattern(const array<array<int, 5>, 5>& pattern, Color color) {
Image img;
img.create(SPRITE_SIZE, SPRITE_SIZE, Color::Transparent);
int scale = SPRITE_SIZE / 5;
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 5; x++) {
if (pattern[y][x] == 1) {
for (int sy = 0; sy < scale; sy++) {
for (int sx = 0; sx < scale; sx++) {
img.setPixel(x * scale + sx, y * scale + sy, color);
}
}
}
}
}
texture.update(img);
}
const Sprite& getSprite() const { return sprite; }
void setPosition(float x, float y) { sprite.setPosition(x, y); }
};
class SoundManager {
private:
SoundBuffer moveBuffer;
SoundBuffer eatBuffer;
SoundBuffer gameOverBuffer;
SoundBuffer milestoneBuffer;
SoundBuffer themeSongBuffer;
Sound moveSound;
Sound eatSound;
Sound gameOverSound;
Sound milestoneSound;
Sound themeSongSound;
bool soundLoaded;
public:
SoundManager() : soundLoaded(true) {
// Create simple beep sounds programmatically
createMoveSound();
createEatSound();
createGameOverSound();
createMilestoneSound();
// createThemeSong();
// Set volumes to be quieter
moveSound.setVolume(20.0f); // Quiet for movement
eatSound.setVolume(30.0f); // Louder for eating
gameOverSound.setVolume(35.0f); // Even louder for game over
milestoneSound.setVolume(40.0f); // More noticeable for milestones
themeSongSound.setVolume(25.0f); // Background music quieter
// Set theme song to loop
themeSongSound.setLoop(true);
}
void createMoveSound() {
const unsigned SAMPLE_RATE = 44100;
const unsigned DURATION = 5000; // Very short duration (in samples)
std::vector<Int16> samples(DURATION);
// Generate a cheerful beep for movement (higher pitch, shorter)
for (unsigned i = 0; i < DURATION; i++) {
double time = i / static_cast<double>(SAMPLE_RATE);
// Major third chord (more cheerful than single tone)
double freq1 = 880; // Base note
double freq2 = 1100; // Major third
samples[i] = 5000 * sin(2 * 3.14159 * freq1 * time) +
5000 * sin(2 * 3.14159 * freq2 * time);
}
// Apply quick fade-in and fade-out (makes it sound less harsh)
for (unsigned i = 0; i < DURATION / 10; i++) {
double fadeIn = static_cast<double>(i) / (DURATION / 10);
samples[i] = static_cast<Int16>(samples[i] * fadeIn);
}
for (unsigned i = DURATION / 2; i < DURATION; i++) {
double fadeOut = 1.0 - (static_cast<double>(i - DURATION / 2) / (DURATION / 2));
samples[i] = static_cast<Int16>(samples[i] * fadeOut);
}
if (!moveBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
soundLoaded = false;
}
moveSound.setBuffer(moveBuffer);
}
void createEatSound() {
const unsigned SAMPLE_RATE = 44100;
const unsigned DURATION = 10000; // Slightly longer duration for eat sound
std::vector<Int16> samples(DURATION);
for (unsigned i = 0; i < DURATION; i++) {
double time = i / static_cast<double>(SAMPLE_RATE);
// Create a C major ascending arpeggio (C, E, G) - happy sound
double phase = (i * 15.0) / DURATION; // Controls arpeggio speed
double note = floor(phase * 3) / 3.0; // 0, 1/3, 2/3
// C major chord frequencies (C, E, G)
double freq;
if (note < 0.34) freq = 523.25; // C5
else if (note < 0.67) freq = 659.25; // E5
else freq = 783.99; // G5
samples[i] = 12000 * sin(2 * 3.14159 * freq * time);
}
// Apply fade-in and fade-out
for (unsigned i = 0; i < DURATION / 5; i++) {
double fadeIn = static_cast<double>(i) / (DURATION / 5);
samples[i] = static_cast<Int16>(samples[i] * fadeIn);
}
for (unsigned i = 4 * DURATION / 5; i < DURATION; i++) {
double fadeOut = 1.0 - (static_cast<double>(i - 4 * DURATION / 5) / (DURATION / 5));
samples[i] = static_cast<Int16>(samples[i] * fadeOut);
}
if (!eatBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
soundLoaded = false;
}
eatSound.setBuffer(eatBuffer);
}
void createGameOverSound() {
const unsigned SAMPLE_RATE = 44100;
const unsigned DURATION = 44100 / 1.2; // Almost a second
std::vector<Int16> samples(DURATION);
for (unsigned i = 0; i < DURATION; i++) {
double time = i / static_cast<double>(SAMPLE_RATE);
double phase = static_cast<double>(i) / DURATION; // 0 to 1
// C major scale descending (C6 down to C5)
double noteFreq;
if (phase < 0.125) noteFreq = 1046.50; // C6
else if (phase < 0.25) noteFreq = 932.33; // Bb5
else if (phase < 0.375) noteFreq = 783.99; // G5
else if (phase < 0.5) noteFreq = 698.46; // F5
else if (phase < 0.625) noteFreq = 659.25; // E5
else if (phase < 0.75) noteFreq = 587.33; // D5
else if (phase < 0.875) noteFreq = 523.25; // C5
else noteFreq = 493.88; // B4
samples[i] = 15000 * sin(2 * 3.14159 * noteFreq * time);
}
// Apply envelope to make it sound more natural
for (unsigned i = 0; i < DURATION; i++) {
double progress = static_cast<double>(i) / DURATION;
double envelope = 1.0 - progress * 0.5; // Gradual fade
samples[i] = static_cast<Int16>(samples[i] * envelope);
}
if (!gameOverBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
soundLoaded = false;
}
gameOverSound.setBuffer(gameOverBuffer);
}
void createMilestoneSound() {
const unsigned SAMPLE_RATE = 44100;
const unsigned DURATION = 44100 / 2; // Half second
std::vector<Int16> samples(DURATION);
for (unsigned i = 0; i < DURATION; i++) {
double time = i / static_cast<double>(SAMPLE_RATE);
double phase = static_cast<double>(i) / DURATION;
double freq1, freq2;
if (phase < 0.3) {
// First chord - C major
freq1 = 523.25; // C5
freq2 = 659.25; // E5
} else if (phase < 0.6) {
// Second chord - F major
freq1 = 698.46; // F5
freq2 = 880.00; // A5
} else {
// Final chord - G major (dominant)
freq1 = 783.99; // G5
freq2 = 987.77; // B5
}
// Mix two frequencies for a richer sound
samples[i] = 8000 * sin(2 * 3.14159 * freq1 * time) +
8000 * sin(2 * 3.14159 * freq2 * time);
}
// Apply envelope to make it sound more natural
for (unsigned i = 0; i < DURATION / 10; i++) {
double fadeIn = static_cast<double>(i) / (DURATION / 10);
samples[i] = static_cast<Int16>(samples[i] * fadeIn);
}
for (unsigned i = 8 * DURATION / 10; i < DURATION; i++) {
double fadeOut = 1.0 - (static_cast<double>(i - 8 * DURATION / 10) / (2 * DURATION / 10));
samples[i] = static_cast<Int16>(samples[i] * fadeOut);
}
if (!milestoneBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
soundLoaded = false;
}
milestoneSound.setBuffer(milestoneBuffer);
}
void createThemeSong() {
const unsigned SAMPLE_RATE = 44100;
const unsigned DURATION = SAMPLE_RATE * 8; // 8 seconds loop
std::vector<Int16> samples(DURATION);
const double pentatonic[] = {
523.25, // C5
587.33, // D5
659.25, // E5
783.99, // G5
880.00 // A5
};
const int numNotes = 5;
// Create a simple melody pattern
const int pattern[] = {0, 2, 4, 2, 3, 1, 0, 1, 2, 4, 3, 2, 1, 3, 2, 0};
const int patternLength = 16;
// Beat durations (in seconds)
const double beatDuration = 0.5; // Half second per beat
const double beatsPerBar = 4; // 4 beats in a bar
for (unsigned i = 0; i < DURATION; i++) {
double time = i / static_cast<double>(SAMPLE_RATE);
double beatPosition = fmod(time, beatDuration * beatsPerBar) / beatDuration; // Position within the bar
int currentBeat = static_cast<int>(time / beatDuration) % patternLength;
// Get current note in the pattern
int noteIndex = pattern[currentBeat];
double freq = pentatonic[noteIndex];
// Add a bass note every 4 beats
double bassFreq = 0;
if (currentBeat % 4 == 0) {
bassFreq = pentatonic[0] / 2; // One octave lower than the root
} else if (currentBeat % 4 == 2) {
bassFreq = pentatonic[2] / 2; // One octave lower than the third
}
// Simple envelope for each note
double envelope = 1.0;
double noteTime = fmod(time, beatDuration);
if (noteTime < 0.05) {
envelope = noteTime / 0.05; // Quick attack
} else if (noteTime > beatDuration * 0.8) {
envelope = (beatDuration - noteTime) / (beatDuration * 0.2); // Release
}
// Mix the sounds
double mainNote = sin(2 * 3.14159 * freq * time);
double bassNote = (bassFreq > 0) ? sin(2 * 3.14159 * bassFreq * time) : 0;
// Quieter theme song for background
samples[i] = static_cast<Int16>(envelope * 6000 * mainNote +
envelope * 4000 * bassNote);
}
if (!themeSongBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
soundLoaded = false;
}
themeSongSound.setBuffer(themeSongBuffer);
}
void playMoveSound() {
if (soundLoaded) {
moveSound.play();
}
}
void playEatSound() {
if (soundLoaded) {
eatSound.play();
}
}
void playGameOverSound() {
if (soundLoaded) {
// Stop theme song when game is over
themeSongSound.stop();
gameOverSound.play();
}
}
void playMilestoneSound() {
if (soundLoaded) {
milestoneSound.play();
}
}
void startThemeSong() {
if (soundLoaded) {
themeSongSound.play();
}
}
void stopThemeSong() {
if (soundLoaded) {
themeSongSound.stop();
}
}
void pauseThemeSong() {
if (soundLoaded) {
themeSongSound.pause();
}
}
void resumeThemeSong() {
if (soundLoaded) {
themeSongSound.play();
}
}
};
class Game {
private:
RenderWindow window;
vector<SnakeSegment> snake;
Vector2f fruit;
Vector2f direction;
bool isMoving;
Clock clock;
float speed;
int score;
int lastMilestone;
Font font;
Text scoreText;
Text messageText;
Clock messageTimer;
bool showMessage;
SoundManager soundManager;
Vector2f lastDirection;
// Numeric sprites
NumericSprite snakeHeadSprite;
NumericSprite snakeBodySprite;
NumericSprite fruitSprite;
// Grid vertex array
VertexArray grid;
// Patterns
// Make it have a lil smile for the head pattern
const array<array<int, 5>, 5> HEAD_PATTERN = {{
{0, 1, 1, 1, 0},
{1, 0, 1, 0, 1},
{1, 1, 0, 1, 1},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1}
}};
const array<array<int, 5>, 5> BODY_PATTERN = {{
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1}
}};
// Apple-like pattern
const array<array<int, 5>, 5> FRUIT_PATTERN = {{
{0, 1, 1, 1, 0},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1},
{1, 1, 1, 1, 1},
{0, 1, 1, 1, 0}
}};
public:
Game() : window(VideoMode(WIDTH, HEIGHT), "cppsnek"), direction(1, 0),
isMoving(false),
speed(0.05f), // Fast asf speed lolz
score(0),
lastMilestone(0),
grid(Lines),
lastDirection(1, 0),
showMessage(false) {
window.setFramerateLimit(60);
// Initialize snake
snake.push_back({GRID_WIDTH / 2, GRID_HEIGHT / 2});
snake.push_back({GRID_WIDTH / 2 - 1, GRID_HEIGHT / 2});
snake.push_back({GRID_WIDTH / 2 - 2, GRID_HEIGHT / 2});
// Create sprites
snakeHeadSprite.createFromPattern(HEAD_PATTERN, Color(200, 200, 0)); // Yellow head
snakeBodySprite.createFromPattern(BODY_PATTERN, Color(0, 200, 0)); // Green body
fruitSprite.createFromPattern(FRUIT_PATTERN, Color(200, 0, 0)); // Red apple
// Create grid
createGrid();
spawnFruit();
// Setup font
if (!font.loadFromFile("/usr/share/fonts/gnu-free/FreeSans.ttf")) {
font.loadFromFile("/usr/share/fonts/liberation/LiberationSans-Regular.ttf");
}
// Setup score text
scoreText.setFont(font);
scoreText.setCharacterSize(24);
scoreText.setFillColor(Color::White);
scoreText.setPosition(10, 10);
updateScoreText();
// Setup milestone message text
messageText.setFont(font);
messageText.setCharacterSize(32);
messageText.setFillColor(Color::Yellow);
messageText.setPosition(WIDTH / 2 - 100, HEIGHT / 2 - 50);
}
void createGrid() {
// Vertical lines
for (int x = 0; x <= WIDTH; x += GRID_SIZE) {
grid.append(Vertex(Vector2f(x, 0), Color(50, 50, 50)));
grid.append(Vertex(Vector2f(x, HEIGHT), Color(50, 50, 50)));
}
// Horizontal lines
for (int y = 0; y <= HEIGHT; y += GRID_SIZE) {
grid.append(Vertex(Vector2f(0, y), Color(50, 50, 50)));
grid.append(Vertex(Vector2f(WIDTH, y), Color(50, 50, 50)));
}
}
void spawnFruit() {
static random_device rd;
static mt19937 gen(rd());
uniform_int_distribution<> distX(0, GRID_WIDTH - 1);
uniform_int_distribution<> distY(0, GRID_HEIGHT - 1);
fruit.x = distX(gen);
fruit.y = distY(gen);
for (const auto& segment : snake) {
if (segment.x == fruit.x && segment.y == fruit.y) {
spawnFruit();
return;
}
}
}
void updateScoreText() {
scoreText.setString("Score: " + to_string(score));
}
void handleInput() {
bool directionChanged = false;
if (Keyboard::isKeyPressed(Keyboard::A) && direction.x == 0) {
direction = Vector2f(-1, 0);
isMoving = true;
directionChanged = true;
}
else if (Keyboard::isKeyPressed(Keyboard::D) && direction.x == 0) {
direction = Vector2f(1, 0);
isMoving = true;
directionChanged = true;
}
else if (Keyboard::isKeyPressed(Keyboard::W) && direction.y == 0) {
direction = Vector2f(0, -1);
isMoving = true;
directionChanged = true;
}
else if (Keyboard::isKeyPressed(Keyboard::S) && direction.y == 0) {
direction = Vector2f(0, 1);
isMoving = true;
directionChanged = true;
}
if (directionChanged && (direction.x != lastDirection.x || direction.y != lastDirection.y)) {
soundManager.playMoveSound();
lastDirection = direction;
}
}
void update() {
if (clock.getElapsedTime().asSeconds() < speed || !isMoving)
return;
clock.restart();
SnakeSegment newHead = {static_cast<int>(snake[0].x + direction.x),
static_cast<int>(snake[0].y + direction.y)};
if (newHead.x < 0 || newHead.x >= GRID_WIDTH || newHead.y < 0 || newHead.y >= GRID_HEIGHT) {
soundManager.playGameOverSound();
reset();
return;
}
for (size_t i = 1; i < snake.size(); i++) {
if (newHead.x == snake[i].x && newHead.y == snake[i].y) {
soundManager.playGameOverSound();
reset();
return;
}
}
if (newHead.x == fruit.x && newHead.y == fruit.y) {
snake.insert(snake.begin(), newHead);
spawnFruit();
score += 10;
updateScoreText();
soundManager.playEatSound();
// Check for milestones (every 100 points)
if (score >= 100 && score / 100 > lastMilestone / 100) {
lastMilestone = score;
showMilestoneMessage();
soundManager.playMilestoneSound();
}
// Speed up the game as score increases
if (score % 50 == 0 && speed > 0.05f) {
speed -= 0.01f;
}
} else {
snake.insert(snake.begin(), newHead);
snake.pop_back();
}
// Update message display time
if (showMessage && messageTimer.getElapsedTime().asSeconds() > 2.0f) {
showMessage = false;
}
}
void showMilestoneMessage() {
messageText.setString("MILESTONE: " + std::to_string(score) + " POINTS!");
showMessage = true;
messageTimer.restart();
}
void reset() {
snake.clear();
snake.push_back({GRID_WIDTH / 2, GRID_HEIGHT / 2});
snake.push_back({GRID_WIDTH / 2 - 1, GRID_HEIGHT / 2});
snake.push_back({GRID_WIDTH / 2 - 2, GRID_HEIGHT / 2});
direction = Vector2f(1, 0);
lastDirection = Vector2f(1, 0);
isMoving = false;
speed = 0.05f; // Same as in rungame
score = 0;
lastMilestone = 0;
updateScoreText();
spawnFruit();
// Restart theme song
soundManager.startThemeSong();
}
void render() {
window.clear(Color(30, 30, 30)); // Dark gray background
// Draw grid first
window.draw(grid);
// Draw snake
for (size_t i = 0; i < snake.size(); i++) {
if (i == 0) {
snakeHeadSprite.setPosition(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE);
window.draw(snakeHeadSprite.getSprite());
} else {
snakeBodySprite.setPosition(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE);
window.draw(snakeBodySprite.getSprite());
}
}
// Draw fruit (apple)
fruitSprite.setPosition(fruit.x * GRID_SIZE, fruit.y * GRID_SIZE);
window.draw(fruitSprite.getSprite());
// Draw score
window.draw(scoreText);
// Draw milestone message if needed
if (showMessage) {
window.draw(messageText);
}
window.display();
}
void run() {
// Don't start theme song when game starts ; cause its fucking terrible
// soundManager.startThemeSong();
while (window.isOpen()) {
Event event;
while (window.pollEvent(event)) {
if (event.type == Event::Closed)
window.close();
// Pause/resume game with space
if (event.type == Event::KeyPressed && event.key.code == Keyboard::Space) {
isMoving = !isMoving;
if (isMoving)
soundManager.resumeThemeSong();
else
soundManager.pauseThemeSong();
}
}
handleInput();
update();
render();
}
}
};
int main() {
Game game;
game.run();
return 0;
}