Initial release: SDL Windows Tetris

This commit is contained in:
2025-08-15 11:01:48 +02:00
commit d161b2c550
196 changed files with 5944 additions and 0 deletions

252
src/Audio.cpp Normal file
View File

@ -0,0 +1,252 @@
// Audio.cpp - Windows Media Foundation MP3 decoding
#include "Audio.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
#include <fstream>
#include <cstring>
#include <vector>
#include <chrono>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <objbase.h>
#include <wrl/client.h>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")
using Microsoft::WRL::ComPtr;
#endif
Audio& Audio::instance(){ static Audio inst; return inst; }
bool Audio::init(){ if(outSpec.freq!=0) return true; outSpec.format=SDL_AUDIO_S16; outSpec.channels=outChannels; outSpec.freq=outRate;
#ifdef _WIN32
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { std::fprintf(stderr,"[Audio] MFStartup failed\n"); } else mfStarted=true; }
#endif
return true; }
#ifdef _WIN32
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
outPCM.clear(); outRate=44100; outCh=2;
ComPtr<IMFSourceReader> reader;
wchar_t wpath[MAX_PATH]; int wlen = MultiByteToWideChar(CP_UTF8,0,path.c_str(),-1,wpath,MAX_PATH); if(!wlen) return false;
if(FAILED(MFCreateSourceReaderFromURL(wpath,nullptr,&reader))) return false;
// Request PCM output
ComPtr<IMFMediaType> outType; MFCreateMediaType(&outType); outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100); outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100*4); outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3); reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
while(true){ DWORD flags=0; ComPtr<IMFSample> sample; if(FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,nullptr,&flags,nullptr,&sample))) break; if(flags & MF_SOURCE_READERF_ENDOFSTREAM) break; if(!sample) continue; ComPtr<IMFMediaBuffer> buffer; if(FAILED(sample->ConvertToContiguousBuffer(&buffer))) continue; BYTE* data=nullptr; DWORD maxLen=0, curLen=0; if(SUCCEEDED(buffer->Lock(&data,&maxLen,&curLen)) && curLen){ size_t samples = curLen/2; size_t oldSz = outPCM.size(); outPCM.resize(oldSz + samples); std::memcpy(outPCM.data()+oldSz, data, curLen); } if(data) buffer->Unlock(); }
outRate=44100; outCh=2; return !outPCM.empty(); }
#endif
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
#ifdef _WIN32
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else std::fprintf(stderr,"[Audio] Failed to decode %s\n", path.c_str());
#else
std::fprintf(stderr,"[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
#endif
tracks.push_back(std::move(t)); }
void Audio::shuffle(){
std::lock_guard<std::mutex> lock(tracksMutex);
std::shuffle(tracks.begin(), tracks.end(), rng);
}
bool Audio::ensureStream(){
if(audioStream) return true;
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this);
if(!audioStream){
std::fprintf(stderr,"[Audio] SDL_OpenAudioDeviceStream failed: %s\n", SDL_GetError());
return false;
}
return true;
}
void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } }
void Audio::toggleMute(){ muted=!muted; }
void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i<tracks.size(); ++i){ current = (current + 1) % (int)tracks.size(); if(tracks[current].ok){ tracks[current].cursor=0; return; } } current=-1; }
void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
if(bytesWanted==0) return;
// Prepare a buffer of int16 samples for the output device
const size_t outSamples = bytesWanted / sizeof(int16_t);
std::vector<int16_t> mix(outSamples, 0);
// 1) Mix music into buffer (if not muted)
if(!muted && current >= 0){
size_t cursorBytes = 0;
while(cursorBytes < bytesWanted){
if(current < 0) break;
auto &trk = tracks[current];
size_t samplesAvail = trk.pcm.size() - trk.cursor; // samples (int16)
if(samplesAvail == 0){ nextTrack(); if(current < 0) break; continue; }
size_t samplesNeeded = (bytesWanted - cursorBytes) / sizeof(int16_t);
size_t toCopy = (samplesAvail < samplesNeeded) ? samplesAvail : samplesNeeded;
if(toCopy == 0) break;
// Mix add with clamp
size_t startSample = cursorBytes / sizeof(int16_t);
for(size_t i=0;i<toCopy;++i){
int v = (int)mix[startSample+i] + (int)trk.pcm[trk.cursor+i];
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[startSample+i] = (int16_t)v;
}
trk.cursor += toCopy;
cursorBytes += (Uint32)(toCopy * sizeof(int16_t));
if(trk.cursor >= trk.pcm.size()) nextTrack();
}
}
// 2) Mix active SFX
{
std::lock_guard<std::mutex> lock(sfxMutex);
for(size_t si=0; si<activeSfx.size(); ){
auto &s = activeSfx[si];
size_t samplesAvail = s.pcm.size() - s.cursor;
if(samplesAvail == 0){ activeSfx.erase(activeSfx.begin()+si); continue; }
size_t toCopy = (samplesAvail < outSamples) ? samplesAvail : outSamples;
for(size_t i=0;i<toCopy;++i){
int v = (int)mix[i] + (int)s.pcm[s.cursor+i];
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[i] = (int16_t)v;
}
s.cursor += toCopy;
++si;
}
}
// Submit mixed audio
if(!mix.empty()) SDL_PutAudioStreamData(stream, mix.data(), (int)bytesWanted);
}
void Audio::playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume){
if(pcm.empty()) return;
if(!ensureStream()) return;
// Convert input to device format (S16, stereo, 44100)
SDL_AudioSpec src{}; src.format=SDL_AUDIO_S16; src.channels=(Uint8)channels; src.freq=rate;
SDL_AudioSpec dst{}; dst.format=SDL_AUDIO_S16; dst.channels=(Uint8)outChannels; dst.freq=outRate;
SDL_AudioStream* cvt = SDL_CreateAudioStream(&src, &dst);
if(!cvt) return;
// Apply volume while copying into a temp buffer
std::vector<int16_t> volBuf(pcm.size());
for(size_t i=0;i<pcm.size();++i){
int v = (int)(pcm[i] * volume);
if(v>32767) v=32767; if(v<-32768) v=-32768; volBuf[i]=(int16_t)v;
}
SDL_PutAudioStreamData(cvt, volBuf.data(), (int)(volBuf.size()*sizeof(int16_t)));
SDL_FlushAudioStream(cvt);
int bytes = SDL_GetAudioStreamAvailable(cvt);
if(bytes>0){
std::vector<int16_t> out(bytes/2);
SDL_GetAudioStreamData(cvt, out.data(), bytes);
std::lock_guard<std::mutex> lock(sfxMutex);
activeSfx.push_back(SfxPlay{ std::move(out), 0 });
}
SDL_DestroyAudioStream(cvt);
}
void SDLCALL Audio::streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total){ Uint32 want = additional>0 ? (Uint32)additional : (Uint32)total; if(!want) want=4096; reinterpret_cast<Audio*>(userdata)->feed(want, stream); }
void Audio::addTrackAsync(const std::string& path) {
std::lock_guard<std::mutex> lock(pendingTracksMutex);
pendingTracks.push_back(path);
}
void Audio::startBackgroundLoading() {
if (loadingThread.joinable()) return; // Already running
loadingComplete = false;
loadedCount = 0;
loadingThread = std::thread(&Audio::backgroundLoadingThread, this);
}
void Audio::backgroundLoadingThread() {
#ifdef _WIN32
// Initialize COM and MF for this thread
HRESULT hrCom = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
HRESULT hrMF = MFStartup(MF_VERSION);
bool mfInitialized = SUCCEEDED(hrMF);
if (!mfInitialized) {
std::fprintf(stderr, "[Audio] Failed to initialize MF on background thread\n");
}
#endif
// Copy pending tracks to avoid holding the mutex during processing
std::vector<std::string> tracksToProcess;
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
tracksToProcess = pendingTracks;
}
for (const std::string& path : tracksToProcess) {
AudioTrack t;
t.path = path;
#ifdef _WIN32
if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) {
t.ok = true;
} else {
std::fprintf(stderr, "[Audio] Failed to decode %s\n", path.c_str());
}
#else
std::fprintf(stderr, "[Audio] MP3 unsupported on this platform (stub): %s\n", path.c_str());
#endif
// Thread-safe addition to tracks
{
std::lock_guard<std::mutex> lock(tracksMutex);
tracks.push_back(std::move(t));
}
loadedCount++;
// Small delay to prevent overwhelming the system
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
#ifdef _WIN32
// Cleanup MF and COM for this thread
if (mfInitialized) {
MFShutdown();
}
if (SUCCEEDED(hrCom)) {
CoUninitialize();
}
#endif
loadingComplete = true;
}
void Audio::waitForLoadingComplete() {
if (loadingThread.joinable()) {
loadingThread.join();
}
}
bool Audio::isLoadingComplete() const {
return loadingComplete;
}
int Audio::getLoadedTrackCount() const {
return loadedCount;
}
void Audio::shutdown(){
// Stop background loading thread first
if (loadingThread.joinable()) {
loadingThread.join();
}
if(audioStream){ SDL_DestroyAudioStream(audioStream); audioStream=nullptr; }
tracks.clear();
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
pendingTracks.clear();
}
playing=false;
#ifdef _WIN32
if(mfStarted){ MFShutdown(); mfStarted=false; }
#endif
}

53
src/Audio.h Normal file
View File

@ -0,0 +1,53 @@
// Audio.h - MP3 playlist playback (Windows Media Foundation backend) + SDL3 stream
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <string>
#include <random>
#include <cstdint>
#include <thread>
#include <mutex>
#include <atomic>
struct AudioTrack { std::string path; std::vector<int16_t> pcm; int channels=2; int rate=44100; size_t cursor=0; bool ok=false; };
class Audio {
public:
static Audio& instance();
bool init(); // initialize backend (MF on Windows)
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100
void addTrackAsync(const std::string& path); // add track for background loading
void startBackgroundLoading(); // start background thread for loading
void waitForLoadingComplete(); // wait for all tracks to finish loading
bool isLoadingComplete() const; // check if background loading is done
int getLoadedTrackCount() const; // get number of tracks loaded so far
void shuffle(); // randomize order
void start(); // begin playback
void toggleMute();
// Queue a sound effect to mix over the music (pcm can be mono/stereo, any rate; will be converted)
void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume);
void shutdown();
private:
Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete;
static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total);
void feed(Uint32 bytesWanted, SDL_AudioStream* stream);
void nextTrack();
bool ensureStream();
void backgroundLoadingThread(); // background thread function
std::vector<AudioTrack> tracks; int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()};
SDL_AudioStream* audioStream=nullptr; SDL_AudioSpec outSpec{}; int outChannels=2; int outRate=44100; bool mfStarted=false;
// Threading support
std::vector<std::string> pendingTracks;
std::thread loadingThread;
std::mutex tracksMutex;
std::mutex pendingTracksMutex;
std::atomic<bool> loadingComplete{false};
std::atomic<int> loadedCount{0};
// SFX mixing support
struct SfxPlay { std::vector<int16_t> pcm; size_t cursor=0; };
std::vector<SfxPlay> activeSfx;
std::mutex sfxMutex;
};

21
src/Font.cpp Normal file
View File

@ -0,0 +1,21 @@
// Font.cpp - implementation of FontAtlas
#include "Font.h"
#include <SDL3/SDL.h>
bool FontAtlas::init(const std::string& path, int basePt) { fontPath = path; baseSize = basePt; return true; }
void FontAtlas::shutdown() { for (auto &kv : cache) if (kv.second) TTF_CloseFont(kv.second); cache.clear(); }
TTF_Font* FontAtlas::getSized(int ptSize) {
auto it = cache.find(ptSize); if (it!=cache.end()) return it->second;
TTF_Font* f = TTF_OpenFont(fontPath.c_str(), ptSize);
if (!f) return nullptr; cache[ptSize] = f; return f;
}
void FontAtlas::draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color) {
if (scale <= 0) return; int pt = int(baseSize * scale); if (pt < 8) pt = 8; TTF_Font* f = getSized(pt); if (!f) return;
SDL_Surface* surf = TTF_RenderText_Blended(f, text.c_str(), text.length(), color); if (!surf) return;
SDL_Texture* tex = SDL_CreateTextureFromSurface(r, surf);
if (tex) { SDL_FRect dst{ x, y, (float)surf->w, (float)surf->h }; SDL_RenderTexture(r, tex, nullptr, &dst); SDL_DestroyTexture(tex); }
SDL_DestroySurface(surf);
}

18
src/Font.h Normal file
View File

@ -0,0 +1,18 @@
// Font.h - Font rendering abstraction with simple size cache
#pragma once
#include <SDL3_ttf/SDL_ttf.h>
#include <string>
#include <unordered_map>
struct SDL_Renderer;
class FontAtlas {
public:
bool init(const std::string& path, int basePt);
void shutdown();
void draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color);
private:
std::string fontPath;
int baseSize{24};
std::unordered_map<int, TTF_Font*> cache; // point size -> font*
TTF_Font* getSized(int ptSize);
};

262
src/Game.cpp Normal file
View File

@ -0,0 +1,262 @@
// Game.cpp - Implementation of core Tetris game logic
#include "Game.h"
#include <algorithm>
#include <cmath>
// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
static const std::array<Shape, PIECE_COUNT> SHAPES = {{
Shape{ 0x0F00, 0x2222, 0x00F0, 0x4444 }, // I
Shape{ 0x0660, 0x0660, 0x0660, 0x0660 }, // O
Shape{ 0x0E40, 0x4C40, 0x4E00, 0x4640 }, // T
Shape{ 0x06C0, 0x4620, 0x06C0, 0x4620 }, // S
Shape{ 0x0C60, 0x2640, 0x0C60, 0x2640 }, // Z
Shape{ 0x08E0, 0x6440, 0x0E20, 0x44C0 }, // J
Shape{ 0x02E0, 0x4460, 0x0E80, 0xC440 }, // L
}};
void Game::reset(int startLevel_) {
std::fill(board.begin(), board.end(), 0);
std::fill(blockCounts.begin(), blockCounts.end(), 0);
bag.clear();
_score = 0; _lines = 0; _level = startLevel_ + 1; startLevel = startLevel_;
gravityMs = 800.0 * std::pow(0.85, startLevel_); // speed-up for higher starts
fallAcc = 0; _elapsedSec = 0; gameOver=false; paused=false;
hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
refillBag(); spawn();
}
void Game::refillBag() {
bag.clear();
for (int i=0;i<PIECE_COUNT;++i) bag.push_back(static_cast<PieceType>(i));
std::shuffle(bag.begin(), bag.end(), rng);
}
void Game::spawn() {
if (bag.empty()) refillBag();
PieceType pieceType = bag.back();
// I-piece needs to start one row higher due to its height when vertical
int spawnY = (pieceType == I) ? -2 : -1;
cur = Piece{ pieceType, 0, 3, spawnY };
// Check if the newly spawned piece collides with existing blocks
if (collides(cur)) {
gameOver = true;
return; // Don't proceed with spawning if it causes a collision
}
bag.pop_back();
blockCounts[cur.type]++; // Increment count for this piece type
canHold = true;
// Prepare next piece
if (bag.empty()) refillBag();
PieceType nextType = bag.back();
int nextSpawnY = (nextType == I) ? -2 : -1;
nextPiece = Piece{ nextType, 0, 3, nextSpawnY };
}
bool Game::cellFilled(const Piece& p, int cx, int cy) {
if (p.type == PIECE_COUNT) return false;
const uint16_t mask = SHAPES[p.type][p.rot];
const int bit = cy*4 + cx;
return (mask >> bit) & 1;
}
bool Game::collides(const Piece& p) const {
for (int cy=0; cy<4; ++cy) {
for (int cx=0; cx<4; ++cx) if (cellFilled(p,cx,cy)) {
int gx = p.x + cx; int gy = p.y + cy;
if (gx < 0 || gx >= COLS || gy >= ROWS) return true;
if (gy >= 0 && board[gy*COLS + gx] != 0) return true;
}
}
return false;
}
void Game::lockPiece() {
for (int cy=0; cy<4; ++cy) {
for (int cx=0; cx<4; ++cx) if (cellFilled(cur,cx,cy)) {
int gx = cur.x + cx; int gy = cur.y + cy;
if (gy >= 0 && gy < ROWS) board[gy*COLS + gx] = static_cast<int>(cur.type)+1;
if (gy < 0) gameOver = true;
}
}
// Check for completed lines but don't clear them yet - let the effect system handle it
int cleared = checkLines();
if (cleared > 0) {
// JS scoring system: base points per clear, multiplied by (level+1) in JS.
// Our _level is 1-based (JS level + 1), so multiplier == _level.
int base = 0;
switch (cleared) {
case 1: base = 40; break; // SINGLE
case 2: base = 100; break; // DOUBLE
case 3: base = 300; break; // TRIPLE
case 4: base = 1200; break; // TETRIS
default: base = 0; break;
}
_score += base * std::max(1, _level);
// Update total lines
_lines += cleared;
// JS level progression (NES-like) using starting level rules
// startLevel is 0-based in JS; our _level is JS level + 1
const int threshold = (startLevel + 1) * 10;
int oldLevel = _level;
// First level up happens when total lines equal threshold
// After that, every 10 lines (when (lines - threshold) % 10 == 0)
if (_lines == threshold) {
_level += 1;
} else if (_lines > threshold && ((_lines - threshold) % 10 == 0)) {
_level += 1;
}
if (_level > oldLevel) {
gravityMs = std::max(60.0, gravityMs * 0.85);
if (levelUpCallback) {
levelUpCallback(_level);
}
}
// Trigger sound effect callback for line clears
if (soundCallback) {
soundCallback(cleared);
}
}
if (!gameOver) spawn();
}
int Game::checkLines() {
completedLines.clear();
// Check each row from bottom to top
for (int y = ROWS - 1; y >= 0; --y) {
bool full = true;
for (int x = 0; x < COLS; ++x) {
if (board[y*COLS + x] == 0) {
full = false;
break;
}
}
if (full) {
completedLines.push_back(y);
}
}
return static_cast<int>(completedLines.size());
}
void Game::clearCompletedLines() {
if (completedLines.empty()) return;
actualClearLines();
completedLines.clear();
}
void Game::actualClearLines() {
if (completedLines.empty()) return;
int write = ROWS - 1;
for (int y = ROWS - 1; y >= 0; --y) {
// Check if this row should be cleared
bool shouldClear = std::find(completedLines.begin(), completedLines.end(), y) != completedLines.end();
if (!shouldClear) {
// Keep this row, move it down if necessary
if (write != y) {
for (int x = 0; x < COLS; ++x) {
board[write*COLS + x] = board[y*COLS + x];
}
}
--write;
}
// If shouldClear is true, we skip this row (effectively removing it)
}
// Clear the top rows that are now empty
for (int y = write; y >= 0; --y) {
for (int x = 0; x < COLS; ++x) {
board[y*COLS + x] = 0;
}
}
}
bool Game::tryMoveDown() {
Piece p = cur; p.y += 1; if (!collides(p)) { cur = p; return true; } return false;
}
void Game::tickGravity(double frameMs) {
if (paused) return; // Don't tick gravity when paused
fallAcc += frameMs;
while (fallAcc >= gravityMs) {
// Attempt to move down by one row
if (tryMoveDown()) {
// Award soft drop points only if player is actively holding Down
// JS: POINTS.SOFT_DROP = 1 per cell for soft drop
if (softDropping) {
_score += 1;
}
} else {
// Can't move down further, lock piece
lockPiece();
if (gameOver) break;
}
fallAcc -= gravityMs;
}
}
void Game::softDropBoost(double frameMs) {
if (!paused) fallAcc += frameMs * 10.0;
}
void Game::hardDrop() {
if (paused) return;
// Count how many rows we drop for scoring parity with JS
int rows = 0;
while (tryMoveDown()) { rows++; }
// JS: POINTS.HARD_DROP = 1 per cell
if (rows > 0) {
_score += rows * 1;
}
lockPiece();
}
void Game::rotate(int dir) {
if (paused) return;
Piece p = cur; p.rot = (p.rot + dir + 4) % 4; const int kicks[5]={0,-1,1,-2,2};
for (int dx : kicks) { p.x = cur.x + dx; if (!collides(p)) { cur = p; return; } }
}
void Game::move(int dx) {
if (paused) return;
Piece p = cur; p.x += dx; if (!collides(p)) cur = p;
}
void Game::holdCurrent() {
if (paused || !canHold) return;
if (hold.type == PIECE_COUNT) {
// First hold - just store current piece and spawn new one
hold = cur;
// I-piece needs to start one row higher due to its height when vertical
int holdSpawnY = (hold.type == I) ? -2 : -1;
hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
spawn();
} else {
// Swap current with held piece
Piece temp = hold;
hold = cur;
// I-piece needs to start one row higher due to its height when vertical
int holdSpawnY = (hold.type == I) ? -2 : -1;
int currentSpawnY = (temp.type == I) ? -2 : -1;
hold.x = 3; hold.y = holdSpawnY; hold.rot = 0;
cur = temp;
cur.x = 3; cur.y = currentSpawnY; cur.rot = 0;
}
canHold = false; // Can only hold once per piece spawn
}

99
src/Game.h Normal file
View File

@ -0,0 +1,99 @@
// Game.h - Core Tetris game logic (board, piece mechanics, scoring events only)
#pragma once
#include <array>
#include <vector>
#include <random>
#include <cstdint>
#include <functional>
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
class Game {
public:
static constexpr int COLS = 10;
static constexpr int ROWS = 20;
static constexpr int TILE = 28; // logical cell size in pixels (render layer decides use)
struct Piece { PieceType type{PIECE_COUNT}; int rot{0}; int x{3}; int y{-2}; };
explicit Game(int startLevel = 0) { reset(startLevel); }
void reset(int startLevel = 0);
// Simulation -----------------------------------------------------------
void tickGravity(double frameMs); // advance gravity accumulator & drop
void softDropBoost(double frameMs); // accelerate fall while held
void hardDrop(); // instant drop & lock
void setSoftDropping(bool on) { softDropping = on; } // mark if player holds Down
void move(int dx); // horizontal move
void rotate(int dir); // +1 cw, -1 ccw (simple wall-kick)
void holdCurrent(); // swap with hold (once per spawn)
// Accessors -----------------------------------------------------------
const std::array<int, COLS*ROWS>& boardRef() const { return board; }
const Piece& current() const { return cur; }
const Piece& next() const { return nextPiece; }
const Piece& held() const { return hold; }
bool canHoldPiece() const { return canHold; }
bool isGameOver() const { return gameOver; }
bool isPaused() const { return paused; }
void setPaused(bool p) { paused = p; }
int score() const { return _score; }
int lines() const { return _lines; }
int level() const { return _level; }
int startLevelBase() const { return startLevel; }
double elapsed() const { return _elapsedSec; }
void addElapsed(double frameMs) { if (!paused) _elapsedSec += frameMs/1000.0; }
// Block statistics
const std::array<int, PIECE_COUNT>& getBlockCounts() const { return blockCounts; }
// Line clearing effects support
bool hasCompletedLines() const { return !completedLines.empty(); }
const std::vector<int>& getCompletedLines() const { return completedLines; }
void clearCompletedLines(); // Actually remove the lines from the board
// Sound effect callbacks
using SoundCallback = std::function<void(int)>; // Callback for line clear sounds (number of lines)
using LevelUpCallback = std::function<void(int)>; // Callback for level up sounds
void setSoundCallback(SoundCallback callback) { soundCallback = callback; }
void setLevelUpCallback(LevelUpCallback callback) { levelUpCallback = callback; }
// Shape helper --------------------------------------------------------
static bool cellFilled(const Piece& p, int cx, int cy);
private:
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
Piece cur{}, hold{}, nextPiece{}; // current, held & next piece
bool canHold{true};
bool paused{false};
std::vector<PieceType> bag; // 7-bag randomizer
std::mt19937 rng{ std::random_device{}() };
std::array<int, PIECE_COUNT> blockCounts{}; // Count of each piece type used
int _score{0};
int _lines{0};
int _level{1};
double gravityMs{800.0};
double fallAcc{0.0};
double _elapsedSec{0.0};
bool gameOver{false};
int startLevel{0};
bool softDropping{false}; // true while player holds Down key
// Line clearing support
std::vector<int> completedLines; // Rows that are complete and ready for effects
// Sound effect callbacks
SoundCallback soundCallback;
LevelUpCallback levelUpCallback;
// Internal helpers ----------------------------------------------------
void refillBag();
void spawn();
bool collides(const Piece& p) const;
void lockPiece();
int checkLines(); // Find completed lines and store them
void actualClearLines(); // Actually remove lines from board
bool tryMoveDown(); // one-row fall; returns true if moved
};

297
src/LineEffect.cpp Normal file
View File

@ -0,0 +1,297 @@
// LineEffect.cpp - Implementation of line clearing visual and audio effects
#include "LineEffect.h"
#include <algorithm>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
LineEffect::Particle::Particle(float px, float py)
: x(px), y(py), size(1.5f + static_cast<float>(rand()) / RAND_MAX * 4.0f), alpha(1.0f) {
// Random velocity for explosive effect
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
float speed = 80.0f + static_cast<float>(rand()) / RAND_MAX * 150.0f; // More explosive speed
vx = std::cos(angle) * speed;
vy = std::sin(angle) * speed - 30.0f; // Less upward bias for more spread
// Bright explosive colors (oranges, yellows, whites, reds)
int colorType = rand() % 4;
switch (colorType) {
case 0: // Orange/Fire
color = {255, static_cast<Uint8>(140 + rand() % 100), static_cast<Uint8>(30 + rand() % 50), 255};
break;
case 1: // Yellow/Gold
color = {255, 255, static_cast<Uint8>(100 + rand() % 155), 255};
break;
case 2: // White
color = {255, 255, 255, 255};
break;
case 3: // Red/Pink
color = {255, static_cast<Uint8>(100 + rand() % 100), static_cast<Uint8>(100 + rand() % 100), 255};
break;
}
}
void LineEffect::Particle::update() {
x += vx * 0.016f; // Assume ~60 FPS
y += vy * 0.016f;
vy += 250.0f * 0.016f; // Stronger gravity for explosive effect
vx *= 0.98f; // Air resistance
alpha -= 0.12f; // Fast fade for explosive burst
if (alpha < 0.0f) alpha = 0.0f;
// Shrink particles as they fade
if (size > 0.5f) size -= 0.05f;
}
void LineEffect::Particle::render(SDL_Renderer* renderer) {
if (alpha <= 0.0f) return;
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 adjustedAlpha = static_cast<Uint8>(alpha * 255.0f);
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha);
// Draw particle as a small circle
for (int i = 0; i < static_cast<int>(size); ++i) {
for (int j = 0; j < static_cast<int>(size); ++j) {
float dx = i - size/2.0f;
float dy = j - size/2.0f;
if (dx*dx + dy*dy <= (size/2.0f)*(size/2.0f)) {
SDL_RenderPoint(renderer, x + dx, y + dy);
}
}
}
}
LineEffect::LineEffect() : renderer(nullptr), state(AnimationState::IDLE), timer(0.0f),
rng(std::random_device{}()), audioStream(nullptr) {
}
LineEffect::~LineEffect() {
shutdown();
}
bool LineEffect::init(SDL_Renderer* r) {
renderer = r;
initAudio();
return true;
}
void LineEffect::shutdown() {
if (audioStream) {
SDL_DestroyAudioStream(audioStream);
audioStream = nullptr;
}
}
void LineEffect::initAudio() {
// For now, we'll generate simple beep sounds procedurally
// In a full implementation, you'd load WAV files
// Generate a simple line clear beep (440Hz for 0.2 seconds)
int sampleRate = 44100;
int duration = static_cast<int>(0.2f * sampleRate);
lineClearSample.resize(duration * 2); // Stereo
for (int i = 0; i < duration; ++i) {
float t = static_cast<float>(i) / sampleRate;
float wave = std::sin(2.0f * M_PI * 440.0f * t) * 0.3f; // 440Hz sine wave
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
lineClearSample[i * 2] = sample; // Left channel
lineClearSample[i * 2 + 1] = sample; // Right channel
}
// Generate a higher pitched tetris sound (880Hz for 0.4 seconds)
duration = static_cast<int>(0.4f * sampleRate);
tetrisSample.resize(duration * 2);
for (int i = 0; i < duration; ++i) {
float t = static_cast<float>(i) / sampleRate;
float wave = std::sin(2.0f * M_PI * 880.0f * t) * 0.4f; // 880Hz sine wave
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
tetrisSample[i * 2] = sample; // Left channel
tetrisSample[i * 2 + 1] = sample; // Right channel
}
}
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize) {
if (rows.empty()) return;
clearingRows = rows;
state = AnimationState::FLASH_WHITE;
timer = 0.0f;
particles.clear();
// Create particles for each clearing row
for (int row : rows) {
createParticles(row, gridX, gridY, blockSize);
}
// Play appropriate sound
playLineClearSound(static_cast<int>(rows.size()));
}
void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
// Create particles spread across the row with explosive pattern
int particlesPerRow = 35; // More particles for dramatic explosion effect
for (int i = 0; i < particlesPerRow; ++i) {
// Create particles along the entire row width
float x = gridX + (static_cast<float>(i) / (particlesPerRow - 1)) * (10 * blockSize);
float y = gridY + row * blockSize + blockSize / 2.0f;
// Add some randomness to position
x += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.8f;
y += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.6f;
particles.emplace_back(x, y);
}
}
bool LineEffect::update(float deltaTime) {
if (state == AnimationState::IDLE) return true;
timer += deltaTime;
switch (state) {
case AnimationState::FLASH_WHITE:
if (timer >= FLASH_DURATION) {
state = AnimationState::EXPLODE_BLOCKS;
timer = 0.0f;
}
break;
case AnimationState::EXPLODE_BLOCKS:
updateParticles();
if (timer >= EXPLODE_DURATION) {
state = AnimationState::BLOCKS_DROP;
timer = 0.0f;
}
break;
case AnimationState::BLOCKS_DROP:
updateParticles();
if (timer >= DROP_DURATION) {
state = AnimationState::IDLE;
clearingRows.clear();
particles.clear();
return true; // Effect complete
}
break;
case AnimationState::IDLE:
return true;
}
return false; // Effect still running
}
void LineEffect::updateParticles() {
// Update all particles
for (auto& particle : particles) {
particle.update();
}
// Remove dead particles
particles.erase(
std::remove_if(particles.begin(), particles.end(),
[](const Particle& p) { return !p.isAlive(); }),
particles.end()
);
}
void LineEffect::render(SDL_Renderer* renderer, int gridX, int gridY, int blockSize) {
if (state == AnimationState::IDLE) return;
switch (state) {
case AnimationState::FLASH_WHITE:
renderFlash(gridX, gridY, blockSize);
break;
case AnimationState::EXPLODE_BLOCKS:
renderExplosion();
break;
case AnimationState::BLOCKS_DROP:
renderExplosion();
break;
case AnimationState::IDLE:
break;
}
}
void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
// Create a flashing white effect with varying opacity
float progress = timer / FLASH_DURATION;
float flashIntensity = std::sin(progress * M_PI * 6.0f) * 0.5f + 0.5f; // Fewer flashes for quicker effect
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 alpha = static_cast<Uint8>(flashIntensity * 180.0f); // Slightly less intense
for (int row : clearingRows) {
// Draw white rectangle covering the entire row with blue glow effect
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
SDL_FRect flashRect = {
static_cast<float>(gridX - 4),
static_cast<float>(gridY + row * blockSize - 4),
static_cast<float>(10 * blockSize + 8),
static_cast<float>(blockSize + 8)
};
SDL_RenderFillRect(renderer, &flashRect);
// Add blue glow border
SDL_SetRenderDrawColor(renderer, 100, 150, 255, alpha / 2);
for (int i = 1; i <= 3; ++i) {
SDL_FRect glowRect = {
flashRect.x - i,
flashRect.y - i,
flashRect.w + 2*i,
flashRect.h + 2*i
};
SDL_RenderRect(renderer, &glowRect);
}
}
}
void LineEffect::renderExplosion() {
for (auto& particle : particles) {
particle.render(renderer);
}
}
void LineEffect::playLineClearSound(int lineCount) {
if (!audioStream) {
// Create audio stream for sound effects
SDL_AudioSpec spec = {};
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = 44100;
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (!audioStream) {
printf("Warning: Could not create audio stream for line clear effects\n");
return;
}
}
// Choose appropriate sound based on line count
const std::vector<int16_t>* sample = nullptr;
if (lineCount == 4) {
sample = &tetrisSample; // Special sound for Tetris
printf("TETRIS! 4 lines cleared!\n");
} else {
sample = &lineClearSample; // Regular line clear sound
printf("Line clear: %d lines\n", lineCount);
}
if (sample && !sample->empty()) {
SDL_PutAudioStreamData(audioStream, sample->data(),
static_cast<int>(sample->size() * sizeof(int16_t)));
}
}

71
src/LineEffect.h Normal file
View File

@ -0,0 +1,71 @@
// LineEffect.h - Line clearing visual and audio effects
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <random>
class LineEffect {
public:
struct Particle {
float x, y;
float vx, vy;
float size;
float alpha;
SDL_Color color;
Particle(float px, float py);
void update();
void render(SDL_Renderer* renderer);
bool isAlive() const { return alpha > 0.0f; }
};
enum class AnimationState {
IDLE,
FLASH_WHITE,
EXPLODE_BLOCKS,
BLOCKS_DROP
};
LineEffect();
~LineEffect();
bool init(SDL_Renderer* renderer);
void shutdown();
// Start line clear effect for the specified rows
void startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize);
// Update and render the effect
bool update(float deltaTime); // Returns true if effect is complete
void render(SDL_Renderer* renderer, int gridX, int gridY, int blockSize);
// Audio
void playLineClearSound(int lineCount);
bool isActive() const { return state != AnimationState::IDLE; }
private:
SDL_Renderer* renderer;
AnimationState state;
float timer;
std::vector<int> clearingRows;
std::vector<Particle> particles;
std::mt19937 rng;
// Audio resources
SDL_AudioStream* audioStream;
std::vector<int16_t> lineClearSample;
std::vector<int16_t> tetrisSample;
// Animation timing - Flash then immediate explosion effect
static constexpr float FLASH_DURATION = 0.12f; // Very brief white flash
static constexpr float EXPLODE_DURATION = 0.15f; // Quick explosive effect
static constexpr float DROP_DURATION = 0.05f; // Almost instant block drop
void createParticles(int row, int gridX, int gridY, int blockSize);
void updateParticles();
void renderFlash(int gridX, int gridY, int blockSize);
void renderExplosion();
bool loadAudioSample(const std::string& path, std::vector<int16_t>& sample);
void initAudio();
};

81
src/Scores.cpp Normal file
View File

@ -0,0 +1,81 @@
// Scores.cpp - Implementation of ScoreManager
#include "Scores.h"
#include <SDL3/SDL.h>
#include <fstream>
#include <sstream>
#include <algorithm>
ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {}
std::string ScoreManager::filePath() const {
static std::string path; if (!path.empty()) return path;
char* base = SDL_GetPrefPath("example","tetris_sdl3");
if (base) { path = std::string(base)+"highscores.txt"; SDL_free(base);} else path="highscores.txt";
return path;
}
void ScoreManager::load() {
scores.clear();
std::ifstream f(filePath());
if (!f) {
// Create sample high scores if file doesn't exist
createSampleScores();
save();
return;
}
std::string line;
while (std::getline(f, line)) {
std::istringstream iss(line);
ScoreEntry e;
iss >> e.score >> e.lines >> e.level >> e.timeSec;
if (iss) {
// Try to read name (rest of line after timeSec)
std::string remaining;
std::getline(iss, remaining);
if (!remaining.empty() && remaining[0] == ' ') {
e.name = remaining.substr(1); // Remove leading space
}
scores.push_back(e);
}
if (scores.size() >= maxEntries) break;
}
if (scores.empty()) {
createSampleScores();
save();
}
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
}
void ScoreManager::save() const {
std::ofstream f(filePath(), std::ios::trunc);
for (auto &e : scores) {
f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << '\n';
}
}
void ScoreManager::submit(int score, int lines, int level, double timeSec) {
scores.push_back(ScoreEntry{score,lines,level,timeSec});
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
if (scores.size()>maxEntries) scores.resize(maxEntries);
save();
}
void ScoreManager::createSampleScores() {
scores = {
{159840, 189, 14, 972, "GREGOR"},
{156340, 132, 12, 714, "GREGOR"},
{155219, 125, 12, 696, "GREGOR"},
{141823, 123, 10, 710, "GREGOR"},
{140079, 71, 11, 410, "GREGOR"},
{116012, 121, 10, 619, "GREGOR"},
{112643, 137, 13, 689, "GREGOR"},
{99190, 61, 10, 378, "GREGOR"},
{93648, 107, 10, 629, "GREGOR"},
{89041, 115, 10, 618, "GREGOR"},
{88600, 55, 9, 354, "GREGOR"},
{86346, 141, 13, 723, "GREGOR"}
};
}

20
src/Scores.h Normal file
View File

@ -0,0 +1,20 @@
// Scores.h - High score persistence manager
#pragma once
#include <vector>
#include <string>
struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; };
class ScoreManager {
public:
explicit ScoreManager(size_t maxScores = 12);
void load();
void save() const;
void submit(int score, int lines, int level, double timeSec);
const std::vector<ScoreEntry>& all() const { return scores; }
private:
std::vector<ScoreEntry> scores;
size_t maxEntries;
std::string filePath() const; // resolve path (SDL pref path or local)
void createSampleScores(); // create sample high scores
};

319
src/SoundEffect.cpp Normal file
View File

@ -0,0 +1,319 @@
// SoundEffect.cpp - Implementation of sound effects system
#include "SoundEffect.h"
#include <SDL3/SDL.h>
#include "Audio.h"
#include <cstdio>
#include <algorithm>
#include <random>
#include <cstring>
#include <memory>
#ifdef _WIN32
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <objbase.h>
#include <wrl/client.h>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")
using Microsoft::WRL::ComPtr;
#endif
// SoundEffect implementation
bool SoundEffect::load(const std::string& filePath) {
// Determine file type by extension
std::string extension = filePath.substr(filePath.find_last_of('.') + 1);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
bool success = false;
if (extension == "wav") {
success = loadWAV(filePath);
} else if (extension == "mp3") {
success = loadMP3(filePath);
} else {
std::fprintf(stderr, "[SoundEffect] Unsupported file format: %s\n", extension.c_str());
return false;
}
if (!success) {
return false;
}
loaded = true;
std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n",
filePath.c_str(), channels, sampleRate, pcmData.size());
return true;
}
void SoundEffect::play(float volume) {
if (!loaded || pcmData.empty()) {
std::printf("[SoundEffect] Cannot play - loaded=%d, pcmData.size()=%zu\n", loaded, pcmData.size());
return;
}
std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
// Calculate final volume
float finalVolume = defaultVolume * volume;
finalVolume = (std::max)(0.0f, (std::min)(1.0f, finalVolume));
// Use the simple audio player to play this sound
SimpleAudioPlayer::instance().playSound(pcmData, channels, sampleRate, finalVolume);
}
void SoundEffect::setVolume(float volume) {
defaultVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
}
// SimpleAudioPlayer implementation
SimpleAudioPlayer& SimpleAudioPlayer::instance() {
static SimpleAudioPlayer inst;
return inst;
}
bool SimpleAudioPlayer::init() {
if (initialized) {
return true;
}
initialized = true;
std::printf("[SimpleAudioPlayer] Initialized\n");
return true;
}
void SimpleAudioPlayer::shutdown() {
initialized = false;
std::printf("[SimpleAudioPlayer] Shut down\n");
}
void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume) {
if (!initialized || pcmData.empty()) {
return;
}
// Route through shared Audio mixer so SFX always play over music
Audio::instance().playSfx(pcmData, channels, sampleRate, volume);
}
bool SoundEffect::loadWAV(const std::string& filePath) {
SDL_AudioSpec wavSpec;
Uint8* wavBuffer;
Uint32 wavLength;
if (!SDL_LoadWAV(filePath.c_str(), &wavSpec, &wavBuffer, &wavLength)) {
std::fprintf(stderr, "[SoundEffect] Failed to load WAV file %s: %s\n",
filePath.c_str(), SDL_GetError());
return false;
}
// Store audio format info
channels = wavSpec.channels;
sampleRate = wavSpec.freq;
// Convert to 16-bit signed if needed
if (wavSpec.format == SDL_AUDIO_S16) {
// Already in the right format
size_t samples = wavLength / sizeof(int16_t);
pcmData.resize(samples);
std::memcpy(pcmData.data(), wavBuffer, wavLength);
} else {
// Need to convert format
SDL_AudioSpec srcSpec = wavSpec;
SDL_AudioSpec dstSpec = wavSpec;
dstSpec.format = SDL_AUDIO_S16;
SDL_AudioStream* converter = SDL_CreateAudioStream(&srcSpec, &dstSpec);
if (converter) {
SDL_PutAudioStreamData(converter, wavBuffer, wavLength);
SDL_FlushAudioStream(converter);
int convertedLength = SDL_GetAudioStreamAvailable(converter);
if (convertedLength > 0) {
pcmData.resize(convertedLength / sizeof(int16_t));
SDL_GetAudioStreamData(converter, pcmData.data(), convertedLength);
}
SDL_DestroyAudioStream(converter);
}
}
SDL_free(wavBuffer);
return !pcmData.empty();
}
bool SoundEffect::loadMP3(const std::string& filePath) {
#ifdef _WIN32
static bool mfInitialized = false;
if (!mfInitialized) {
if (FAILED(MFStartup(MF_VERSION))) {
std::fprintf(stderr, "[SoundEffect] MFStartup failed\n");
return false;
}
mfInitialized = true;
}
ComPtr<IMFSourceReader> reader;
wchar_t wpath[MAX_PATH];
int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath.c_str(), -1, wpath, MAX_PATH);
if (!wlen) {
std::fprintf(stderr, "[SoundEffect] Failed to convert path to wide char\n");
return false;
}
if (FAILED(MFCreateSourceReaderFromURL(wpath, nullptr, &reader))) {
std::fprintf(stderr, "[SoundEffect] Failed to create source reader for %s\n", filePath.c_str());
return false;
}
// Request PCM output
ComPtr<IMFMediaType> outType;
MFCreateMediaType(&outType);
outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2);
outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100);
outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4);
outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100 * 4);
outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16);
outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3);
reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
// Read all samples
std::vector<int16_t> tempData;
while (true) {
DWORD flags = 0;
ComPtr<IMFSample> sample;
if (FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nullptr, &flags, nullptr, &sample))) {
break;
}
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) {
break;
}
if (!sample) {
continue;
}
ComPtr<IMFMediaBuffer> buffer;
if (FAILED(sample->ConvertToContiguousBuffer(&buffer))) {
continue;
}
BYTE* data = nullptr;
DWORD maxLen = 0, curLen = 0;
if (SUCCEEDED(buffer->Lock(&data, &maxLen, &curLen)) && curLen) {
size_t samples = curLen / 2;
size_t oldSize = tempData.size();
tempData.resize(oldSize + samples);
std::memcpy(tempData.data() + oldSize, data, curLen);
}
if (data) {
buffer->Unlock();
}
}
if (tempData.empty()) {
std::fprintf(stderr, "[SoundEffect] No audio data decoded from %s\n", filePath.c_str());
return false;
}
pcmData = std::move(tempData);
channels = 2;
sampleRate = 44100;
return true;
#else
std::fprintf(stderr, "[SoundEffect] MP3 support not available on this platform\n");
return false;
#endif
}
// SoundEffectManager implementation
SoundEffectManager& SoundEffectManager::instance() {
static SoundEffectManager inst;
return inst;
}
bool SoundEffectManager::init() {
if (initialized) {
return true;
}
// Initialize the simple audio player
SimpleAudioPlayer::instance().init();
initialized = true;
std::printf("[SoundEffectManager] Initialized\n");
return true;
}
void SoundEffectManager::shutdown() {
soundEffects.clear();
SimpleAudioPlayer::instance().shutdown();
initialized = false;
std::printf("[SoundEffectManager] Shut down\n");
}
bool SoundEffectManager::loadSound(const std::string& id, const std::string& filePath) {
if (!initialized) {
std::fprintf(stderr, "[SoundEffectManager] Not initialized\n");
return false;
}
auto soundEffect = std::make_unique<SoundEffect>();
if (!soundEffect->load(filePath)) {
std::fprintf(stderr, "[SoundEffectManager] Failed to load sound: %s\n", filePath.c_str());
return false;
}
// Remove existing sound with the same ID
soundEffects.erase(
std::remove_if(soundEffects.begin(), soundEffects.end(),
[&id](const auto& pair) { return pair.first == id; }),
soundEffects.end());
soundEffects.emplace_back(id, std::move(soundEffect));
std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
return true;
}
void SoundEffectManager::playSound(const std::string& id, float volume) {
if (!enabled || !initialized) {
return;
}
auto it = std::find_if(soundEffects.begin(), soundEffects.end(),
[&id](const auto& pair) { return pair.first == id; });
if (it != soundEffects.end()) {
it->second->play(volume * masterVolume);
} else {
std::fprintf(stderr, "[SoundEffectManager] Sound not found: %s\n", id.c_str());
}
}
void SoundEffectManager::playRandomSound(const std::vector<std::string>& soundIds, float volume) {
if (!enabled || !initialized || soundIds.empty()) {
return;
}
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, soundIds.size() - 1);
const std::string& selectedId = soundIds[dis(gen)];
playSound(selectedId, volume);
}
void SoundEffectManager::setMasterVolume(float volume) {
masterVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
}
void SoundEffectManager::setEnabled(bool enabled_) {
enabled = enabled_;
}
bool SoundEffectManager::isEnabled() const {
return enabled;
}

94
src/SoundEffect.h Normal file
View File

@ -0,0 +1,94 @@
// SoundEffect.h - Single sound effect player using SDL3 audio
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
class SoundEffect {
public:
SoundEffect() = default;
~SoundEffect() = default;
// Load a sound effect from file (WAV or MP3)
bool load(const std::string& filePath);
// Play the sound effect
void play(float volume = 1.0f);
// Set default volume for this sound effect
void setVolume(float volume);
// Get PCM data for mixing
const std::vector<int16_t>& getPCMData() const { return pcmData; }
int getChannels() const { return channels; }
int getSampleRate() const { return sampleRate; }
bool isLoaded() const { return loaded; }
private:
std::vector<int16_t> pcmData;
int channels = 2;
int sampleRate = 44100;
bool loaded = false;
float defaultVolume = 1.0f;
bool loadWAV(const std::string& filePath);
bool loadMP3(const std::string& filePath);
};
// Simple audio player for immediate playback
class SimpleAudioPlayer {
public:
static SimpleAudioPlayer& instance();
bool init();
void shutdown();
// Play a sound effect immediately
void playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume = 1.0f);
private:
SimpleAudioPlayer() = default;
~SimpleAudioPlayer() = default;
SimpleAudioPlayer(const SimpleAudioPlayer&) = delete;
SimpleAudioPlayer& operator=(const SimpleAudioPlayer&) = delete;
bool initialized = false;
};
// Helper class to manage multiple sound effects
class SoundEffectManager {
public:
static SoundEffectManager& instance();
bool init();
void shutdown();
// Load a sound effect and assign it an ID
bool loadSound(const std::string& id, const std::string& filePath);
// Play a sound effect by ID
void playSound(const std::string& id, float volume = 1.0f);
// Play a random sound from a group
void playRandomSound(const std::vector<std::string>& soundIds, float volume = 1.0f);
// Set master volume for all sound effects
void setMasterVolume(float volume);
// Enable/disable sound effects
void setEnabled(bool enabled);
bool isEnabled() const;
private:
SoundEffectManager() = default;
~SoundEffectManager() = default;
SoundEffectManager(const SoundEffectManager&) = delete;
SoundEffectManager& operator=(const SoundEffectManager&) = delete;
std::vector<std::pair<std::string, std::unique_ptr<SoundEffect>>> soundEffects;
float masterVolume = 1.0f;
bool enabled = true;
bool initialized = false;
};

41
src/Starfield.cpp Normal file
View File

@ -0,0 +1,41 @@
// Starfield.cpp - implementation
#include "Starfield.h"
#include <SDL3/SDL.h>
#include <random>
void Starfield::init(int count, int w, int h)
{
stars.clear();
stars.reserve(count);
std::mt19937 rng{std::random_device{}()};
std::uniform_real_distribution<float> dx(0.f, (float)w), dy(0.f, (float)h), dz(0.2f, 1.f);
for (int i = 0; i < count; ++i)
stars.push_back({dx(rng), dy(rng), dz(rng), 15.f + 35.f * dz(rng)});
lastW = w;
lastH = h;
}
void Starfield::update(float dt, int w, int h)
{
if (w != lastW || h != lastH || stars.empty())
init((w * h) / 8000 + 120, w, h);
for (auto &s : stars)
{
s.y += s.speed * dt;
if (s.y > h)
{
s.y -= h;
s.x = (float)(rand() % w);
}
}
}
void Starfield::draw(SDL_Renderer *r) const
{
SDL_SetRenderDrawColor(r, 255, 255, 255, 255);
for (auto &s : stars)
{
SDL_FRect fr{s.x, s.y, 1.f * s.z, 1.f * s.z};
SDL_RenderFillRect(r, &fr);
}
}

15
src/Starfield.h Normal file
View File

@ -0,0 +1,15 @@
// Starfield.h - Procedural starfield background effect
#pragma once
#include <vector>
struct SDL_Renderer; // fwd
class Starfield {
public:
void init(int count, int w, int h);
void update(float dt, int w, int h);
void draw(SDL_Renderer* r) const;
private:
struct Star { float x,y,z,speed; };
std::vector<Star> stars;
int lastW{0}, lastH{0};
};

164
src/Starfield3D.cpp Normal file
View File

@ -0,0 +1,164 @@
// Starfield3D.cpp - 3D Parallax Starfield Implementation
#include "Starfield3D.h"
#include <cmath>
#include <algorithm>
Starfield3D::Starfield3D() : rng(std::random_device{}()), width(800), height(600), centerX(400), centerY(300) {
}
void Starfield3D::init(int w, int h, int starCount) {
width = w;
height = h;
centerX = width * 0.5f;
centerY = height * 0.5f;
stars.resize(starCount);
createStarfield();
}
void Starfield3D::resize(int w, int h) {
width = w;
height = h;
centerX = width * 0.5f;
centerY = height * 0.5f;
}
float Starfield3D::randomFloat(float min, float max) {
std::uniform_real_distribution<float> dist(min, max);
return dist(rng);
}
int Starfield3D::randomRange(int min, int max) {
std::uniform_int_distribution<int> dist(min, max - 1);
return dist(rng);
}
void Starfield3D::setRandomDirection(Star3D& star) {
star.targetVx = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
star.targetVy = randomFloat(-MAX_VELOCITY, MAX_VELOCITY);
// Allow stars to move both toward and away from viewer
if (randomFloat(0.0f, 1.0f) < REVERSE_PROBABILITY) {
// Move away from viewer (positive Z)
star.targetVz = STAR_SPEED * randomFloat(0.5f, 1.0f);
} else {
// Move toward viewer (negative Z)
star.targetVz = -STAR_SPEED * randomFloat(0.7f, 1.3f);
}
star.changing = true;
star.changeTimer = randomFloat(30.0f, 120.0f); // Direction change lasts 30-120 frames
}
void Starfield3D::updateStar(int index) {
Star3D& star = stars[index];
star.x = randomFloat(-25.0f, 25.0f);
star.y = randomFloat(-25.0f, 25.0f);
star.z = randomFloat(1.0f, MAX_DEPTH);
// Give stars initial velocities in all possible directions
if (randomFloat(0.0f, 1.0f) < 0.5f) {
// Half stars start moving toward viewer
star.vx = randomFloat(-0.1f, 0.1f);
star.vy = randomFloat(-0.1f, 0.1f);
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
} else {
// Half stars start moving in random directions
star.vx = randomFloat(-0.2f, 0.2f);
star.vy = randomFloat(-0.2f, 0.2f);
// 30% chance to start moving away
if (randomFloat(0.0f, 1.0f) < 0.3f) {
star.vz = STAR_SPEED * randomFloat(0.5f, 0.8f);
} else {
star.vz = -STAR_SPEED * randomFloat(0.8f, 1.2f);
}
}
star.targetVx = star.vx;
star.targetVy = star.vy;
star.targetVz = star.vz;
star.changing = false;
star.changeTimer = 0.0f;
star.type = randomRange(0, COLOR_COUNT);
// Give some stars initial direction variations
if (randomFloat(0.0f, 1.0f) < 0.4f) {
setRandomDirection(star);
}
}
void Starfield3D::createStarfield() {
for (size_t i = 0; i < stars.size(); ++i) {
updateStar(static_cast<int>(i));
}
}
void Starfield3D::update(float deltaTime) {
const float frameRate = 60.0f; // Target 60 FPS for consistency
const float frameMultiplier = deltaTime * frameRate;
for (size_t i = 0; i < stars.size(); ++i) {
Star3D& star = stars[i];
// Randomly change direction occasionally
if (!star.changing && randomFloat(0.0f, 1.0f) < DIRECTION_CHANGE_PROBABILITY * frameMultiplier) {
setRandomDirection(star);
}
// Update velocities to approach target values
if (star.changing) {
// Smoothly transition to target velocities
const float change = VELOCITY_CHANGE * frameMultiplier;
star.vx += (star.targetVx - star.vx) * change;
star.vy += (star.targetVy - star.vy) * change;
star.vz += (star.targetVz - star.vz) * change;
// Decrement change timer
star.changeTimer -= frameMultiplier;
if (star.changeTimer <= 0.0f) {
star.changing = false;
}
}
// Update position using current velocity
star.x += star.vx * frameMultiplier;
star.y += star.vy * frameMultiplier;
star.z += star.vz * frameMultiplier;
// Handle boundaries - reset star if it moves out of bounds, too close, or too far
if (star.z <= MIN_Z ||
star.z >= MAX_Z ||
std::abs(star.x) > 50.0f ||
std::abs(star.y) > 50.0f) {
updateStar(static_cast<int>(i));
}
}
}
void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, int type) {
const SDL_Color& color = STAR_COLORS[type % COLOR_COUNT];
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
// Draw star as a small rectangle (1x1 pixel)
SDL_FRect rect{x, y, 1.0f, 1.0f};
SDL_RenderFillRect(renderer, &rect);
}
void Starfield3D::draw(SDL_Renderer* renderer) {
for (const Star3D& star : stars) {
// Calculate perspective projection factor
const float k = DEPTH_FACTOR / star.z;
// Calculate screen position with perspective
const float px = star.x * k + centerX;
const float py = star.y * k + centerY;
// Only draw stars that are within the viewport
if (px >= 0.0f && px <= static_cast<float>(width) &&
py >= 0.0f && py <= static_cast<float>(height)) {
drawStar(renderer, px, py, star.type);
}
}
}

77
src/Starfield3D.h Normal file
View File

@ -0,0 +1,77 @@
// Starfield3D.h - 3D Parallax Starfield Effect
// Creates a parallax starfield effect that simulates 3D space movement
// By projecting 2D coordinates into 3D space with perspective
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <random>
class Starfield3D {
public:
Starfield3D();
~Starfield3D() = default;
// Initialize the starfield with dimensions
void init(int width, int height, int starCount = 160);
// Update starfield animation (call every frame)
void update(float deltaTime);
// Draw the starfield
void draw(SDL_Renderer* renderer);
// Update dimensions when window resizes
void resize(int width, int height);
private:
// Star representation in 3D space
struct Star3D {
float x, y, z; // 3D position
float vx, vy, vz; // Current velocities
float targetVx, targetVy, targetVz; // Target velocities for smooth transitions
bool changing; // Whether star is currently changing direction
float changeTimer; // Timer for direction change duration
int type; // Star type (determines color/brightness)
Star3D() : x(0), y(0), z(0), vx(0), vy(0), vz(0),
targetVx(0), targetVy(0), targetVz(0),
changing(false), changeTimer(0), type(0) {}
};
// Configuration constants
static constexpr float MAX_DEPTH = 32.0f;
static constexpr float STAR_SPEED = 0.4f;
static constexpr float DEPTH_FACTOR = 256.0f;
static constexpr float MIN_Z = 0.1f;
static constexpr float MAX_Z = 50.0f;
static constexpr float DIRECTION_CHANGE_PROBABILITY = 0.008f;
static constexpr float MAX_VELOCITY = 0.3f;
static constexpr float VELOCITY_CHANGE = 0.03f;
static constexpr float REVERSE_PROBABILITY = 0.4f;
// Private methods
void createStarfield();
void updateStar(int index);
void setRandomDirection(Star3D& star);
float randomFloat(float min, float max);
int randomRange(int min, int max);
void drawStar(SDL_Renderer* renderer, float x, float y, int type);
// Member variables
std::vector<Star3D> stars;
std::mt19937 rng;
int width, height;
float centerX, centerY;
// Star colors (RGB values)
static constexpr SDL_Color STAR_COLORS[] = {
{255, 255, 255, 255}, // White
{170, 170, 170, 255}, // Light gray
{153, 153, 153, 255}, // Medium gray
{119, 119, 119, 255}, // Dark gray
{85, 85, 85, 255} // Very dark gray
};
static constexpr int COLOR_COUNT = 5;
};

57
src/StateManager.h Normal file
View File

@ -0,0 +1,57 @@
// StateManager.h - typed app state router with lifecycle hooks
#pragma once
#include <functional>
#include <unordered_map>
#include <SDL3/SDL.h>
enum class AppState {
Loading = 0,
Menu = 1,
LevelSelect = 2,
Playing = 3,
GameOver = 4,
Settings = 5
};
class StateManager {
public:
using EventHandler = std::function<void(const SDL_Event&)>;
using LifecycleHook = std::function<void()>;
explicit StateManager(AppState initial = AppState::Loading) : current(initial) {}
void registerHandler(AppState state, EventHandler handler) {
handlers[static_cast<int>(state)] = std::move(handler);
}
void registerOnEnter(AppState state, LifecycleHook hook) {
onEnterHooks[static_cast<int>(state)] = std::move(hook);
}
void registerOnExit(AppState state, LifecycleHook hook) {
onExitHooks[static_cast<int>(state)] = std::move(hook);
}
void setState(AppState state) {
// Call exit hook for current state
auto it = onExitHooks.find(static_cast<int>(current));
if (it != onExitHooks.end() && it->second) it->second();
current = state;
// Call enter hook for new state
auto it2 = onEnterHooks.find(static_cast<int>(current));
if (it2 != onEnterHooks.end() && it2->second) it2->second();
}
AppState getState() const { return current; }
void handleEvent(const SDL_Event& e) const {
auto it = handlers.find(static_cast<int>(current));
if (it != handlers.end() && it->second) it->second(e);
}
private:
AppState current;
std::unordered_map<int, EventHandler> handlers;
std::unordered_map<int, LifecycleHook> onEnterHooks;
std::unordered_map<int, LifecycleHook> onExitHooks;
};

1633
src/main.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
// LoadingState.cpp
#include "LoadingState.h"
#include "../Game.h"
#include <SDL3/SDL.h>
#include <cstdio>
LoadingState::LoadingState(StateContext& ctx) : State(ctx) {}
void LoadingState::onEnter() {
loadStart = SDL_GetTicks();
loadingProgress = 0.0;
musicLoaded = false;
currentTrackLoading = 0;
totalTracks = 0;
// Kick off audio loading if available
if (ctx.game) {
// audio initialization handled elsewhere (main still creates Audio)
}
}
void LoadingState::onExit() {
}
void LoadingState::handleEvent(const SDL_Event& e) {
(void)e; // no direct event handling in loading screen
}
void LoadingState::update(double frameMs) {
// Progress calculation is done in main; keep this simple
// This stub allows later migration of Audio::background loading
loadingProgress = std::min(1.0, loadingProgress + frameMs / 1000.0);
}
void LoadingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
(void)renderer; (void)logicalScale; (void)logicalVP;
// Rendering is still performed in main for now; this placeholder keeps the API.
}

20
src/states/LoadingState.h Normal file
View File

@ -0,0 +1,20 @@
// LoadingState.h
#pragma once
#include "State.h"
class LoadingState : public State {
public:
LoadingState(StateContext& ctx);
void onEnter() override;
void onExit() override;
void handleEvent(const SDL_Event& e) override;
void update(double frameMs) override;
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
private:
double loadingProgress = 0.0;
Uint64 loadStart = 0;
bool musicLoaded = false;
int currentTrackLoading = 0;
int totalTracks = 0;
};

33
src/states/MenuState.cpp Normal file
View File

@ -0,0 +1,33 @@
// MenuState.cpp
#include "MenuState.h"
#include "../Scores.h"
#include "../Font.h"
#include <SDL3/SDL.h>
#include <cstdio>
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
void MenuState::onEnter() {
// nothing for now
}
void MenuState::onExit() {
}
void MenuState::handleEvent(const SDL_Event& e) {
// Menu-specific key handling moved from main; main still handles mouse for now
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
if (ctx.startLevelSelection && *ctx.startLevelSelection >= 0) {
// keep simple: allow L/S toggles handled globally in main for now
}
}
}
void MenuState::update(double frameMs) {
(void)frameMs;
}
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
(void)renderer; (void)logicalScale; (void)logicalVP;
// Main still performs actual rendering for now; this placeholder keeps the API.
}

13
src/states/MenuState.h Normal file
View File

@ -0,0 +1,13 @@
// MenuState.h
#pragma once
#include "State.h"
class MenuState : public State {
public:
MenuState(StateContext& ctx);
void onEnter() override;
void onExit() override;
void handleEvent(const SDL_Event& e) override;
void update(double frameMs) override;
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
};

51
src/states/State.h Normal file
View File

@ -0,0 +1,51 @@
// State.h - base class and shared context for app states
#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <vector>
// Forward declarations for frequently used types
class Game;
class ScoreManager;
class Starfield;
class Starfield3D;
class FontAtlas;
class LineEffect;
// Shared context passed to states so they can access common resources
struct StateContext {
// Core subsystems (may be null if not available)
Game* game = nullptr;
ScoreManager* scores = nullptr;
Starfield* starfield = nullptr;
Starfield3D* starfield3D = nullptr;
FontAtlas* font = nullptr;
FontAtlas* pixelFont = nullptr;
LineEffect* lineEffect = nullptr;
// Textures
SDL_Texture* logoTex = nullptr;
SDL_Texture* backgroundTex = nullptr;
SDL_Texture* blocksTex = nullptr;
// Audio / SFX - forward declared types in main
// Pointers to booleans/flags used by multiple states
bool* musicEnabled = nullptr;
int* startLevelSelection = nullptr;
int* hoveredButton = nullptr;
};
class State {
public:
explicit State(StateContext& ctx) : ctx(ctx) {}
virtual ~State() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void handleEvent(const SDL_Event& e) {}
virtual void update(double frameMs) {}
virtual void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {}
protected:
StateContext& ctx;
};

17
src/tetris.code-workspace Normal file
View File

@ -0,0 +1,17 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../../../games/Tetris"
}
],
"settings": {
"workbench.colorCustomizations": {
"activityBar.background": "#59140D",
"titleBar.activeBackground": "#7D1D12",
"titleBar.activeForeground": "#FFFCFC"
}
}
}