#include #include #include #include #include // 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, 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 samples(DURATION); // Generate a cheerful beep for movement (higher pitch, shorter) for (unsigned i = 0; i < DURATION; i++) { double time = i / static_cast(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(i) / (DURATION / 10); samples[i] = static_cast(samples[i] * fadeIn); } for (unsigned i = DURATION / 2; i < DURATION; i++) { double fadeOut = 1.0 - (static_cast(i - DURATION / 2) / (DURATION / 2)); samples[i] = static_cast(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 samples(DURATION); for (unsigned i = 0; i < DURATION; i++) { double time = i / static_cast(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(i) / (DURATION / 5); samples[i] = static_cast(samples[i] * fadeIn); } for (unsigned i = 4 * DURATION / 5; i < DURATION; i++) { double fadeOut = 1.0 - (static_cast(i - 4 * DURATION / 5) / (DURATION / 5)); samples[i] = static_cast(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 samples(DURATION); for (unsigned i = 0; i < DURATION; i++) { double time = i / static_cast(SAMPLE_RATE); double phase = static_cast(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(i) / DURATION; double envelope = 1.0 - progress * 0.5; // Gradual fade samples[i] = static_cast(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 samples(DURATION); for (unsigned i = 0; i < DURATION; i++) { double time = i / static_cast(SAMPLE_RATE); double phase = static_cast(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(i) / (DURATION / 10); samples[i] = static_cast(samples[i] * fadeIn); } for (unsigned i = 8 * DURATION / 10; i < DURATION; i++) { double fadeOut = 1.0 - (static_cast(i - 8 * DURATION / 10) / (2 * DURATION / 10)); samples[i] = static_cast(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 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(SAMPLE_RATE); double beatPosition = fmod(time, beatDuration * beatsPerBar) / beatDuration; // Position within the bar int currentBeat = static_cast(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(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 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, 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, 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, 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(snake[0].x + direction.x), static_cast(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; }