Browse Source

Initial commit of Miyoo Mini Plus port for CyberMatris

main
Matteo Benedetto 1 month ago
commit
e0c2cd210a
  1. 13
      .gitignore
  2. 108
      Font.hpp
  3. 450
      Game.cpp
  4. 189
      Game.hpp
  5. 28
      Makefile
  6. 109
      Makefile.miyoo
  7. 845
      Renderer.cpp
  8. 74
      Renderer.hpp
  9. 802
      Synth.cpp
  10. 125
      Synth.hpp
  11. 50
      launch_miyoo.sh
  12. 332
      main.cpp
  13. BIN
      screenshot.png

13
.gitignore vendored

@ -0,0 +1,13 @@
# Build artifacts
*.o
cybermatris
cybermatris_miyoo
dist_miyoo/
# System / IDE files
.DS_Store
.vscode/
.idea/
# Large screenshot BMP (keep png instead)
screenshot.bmp

108
Font.hpp

@ -0,0 +1,108 @@
#pragma once
#include <cstdint>
// A complete 8x8 bitmap font layout for printable ASCII characters from 32 (space) to 126 (~).
// Total: 95 characters. Each character is represented by 8 bytes.
// Each byte represents one row of 8 pixels from left to right (MSB to LSB).
namespace Font {
constexpr int WIDTH = 8;
constexpr int HEIGHT = 8;
inline const uint8_t BITMAP[95][8] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 32 (space)
{0x18, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x00}, // 33 !
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 34 "
{0x36, 0x36, 0x7f, 0x36, 0x7f, 0x36, 0x36, 0x00}, // 35 #
{0x1c, 0x3e, 0x61, 0x3c, 0x07, 0x83, 0x7c, 0x38}, // 36 $
{0x63, 0x66, 0x0c, 0x18, 0x30, 0x66, 0xc6, 0x00}, // 37 %
{0x38, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0x76, 0x00}, // 38 &
{0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}, // 39 '
{0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00}, // 40 (
{0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00}, // 41 )
{0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00}, // 42 *
{0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00}, // 43 +
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30}, // 44 ,
{0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00}, // 45 -
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}, // 46 .
{0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x00}, // 47 /
{0x3e, 0x63, 0x63, 0x6b, 0x63, 0x63, 0x3e, 0x00}, // 48 0
{0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 49 1
{0x3e, 0x63, 0x07, 0x0e, 0x1c, 0x38, 0x7f, 0x00}, // 50 2
{0x3e, 0x63, 0x07, 0x1e, 0x07, 0x63, 0x3e, 0x00}, // 51 3
{0x06, 0x0e, 0x1e, 0x36, 0x7f, 0x06, 0x06, 0x00}, // 52 4
{0x7f, 0x60, 0x7e, 0x03, 0x03, 0x63, 0x3e, 0x00}, // 53 5
{0x1e, 0x30, 0x60, 0x7e, 0x63, 0x63, 0x3e, 0x00}, // 54 6
{0x7f, 0x63, 0x03, 0x06, 0x0c, 0x18, 0x18, 0x00}, // 55 7
{0x3e, 0x63, 0x63, 0x3e, 0x63, 0x63, 0x3e, 0x00}, // 56 8
{0x3e, 0x63, 0x63, 0x7f, 0x03, 0x06, 0x3c, 0x00}, // 57 9
{0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00}, // 58 :
{0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x00}, // 59 ;
{0x06, 0x0c, 0x18, 0x30, 0x18, 0x0c, 0x06, 0x00}, // 60 <
{0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00}, // 61 =
{0x60, 0x30, 0x18, 0x0c, 0x18, 0x30, 0x60, 0x00}, // 62 >
{0x3e, 0x63, 0x07, 0x0e, 0x18, 0x00, 0x18, 0x00}, // 63 ?
{0x3e, 0x63, 0x6b, 0x6b, 0x6b, 0x3e, 0x00, 0x00}, // 64 @
{0x18, 0x3c, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x00}, // 65 A
{0x7c, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x7c, 0x00}, // 66 B
{0x3e, 0x63, 0x60, 0x60, 0x60, 0x63, 0x3e, 0x00}, // 67 C
{0x78, 0x6c, 0x66, 0x66, 0x66, 0x6c, 0x78, 0x00}, // 68 D
{0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x7f, 0x00}, // 69 E
{0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 70 F
{0x3e, 0x63, 0x60, 0x6f, 0x63, 0x63, 0x3e, 0x00}, // 71 G
{0x66, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00}, // 72 H
{0x3e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3e, 0x00}, // 73 I
{0x1f, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3c, 0x00}, // 74 J
{0x66, 0x6c, 0x78, 0x70, 0x78, 0x6c, 0x66, 0x00}, // 75 K
{0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7f, 0x00}, // 76 L
{0x63, 0x77, 0x7f, 0x6b, 0x63, 0x63, 0x63, 0x00}, // 77 M
{0x63, 0x67, 0x6f, 0x7b, 0x73, 0x63, 0x63, 0x00}, // 78 N
{0x3e, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 79 O
{0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 80 P
{0x3e, 0x63, 0x63, 0x63, 0x6b, 0x6c, 0x3e, 0x03}, // 81 Q
{0x7c, 0x66, 0x66, 0x7c, 0x78, 0x6c, 0x66, 0x00}, // 82 R
{0x3e, 0x63, 0x60, 0x3e, 0x03, 0x63, 0x3e, 0x00}, // 83 S
{0x7f, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x00}, // 84 T
{0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 85 U
{0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1c, 0x00}, // 86 V
{0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00}, // 87 W
{0x63, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x63, 0x00}, // 88 X
{0x63, 0x63, 0x63, 0x3e, 0x0c, 0x0c, 0x0c, 0x00}, // 89 Y
{0x7f, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x7f, 0x00}, // 90 Z
{0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00}, // 91 [
{0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00}, // 92 \
{0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00}, // 93 ]
{0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00}, // 94 ^
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}, // 95 _
{0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // 96 `
{0x00, 0x00, 0x3c, 0x06, 0x3e, 0x66, 0x3b, 0x00}, // 97 a
{0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x7c, 0x00}, // 98 b
{0x00, 0x00, 0x3c, 0x66, 0x60, 0x66, 0x3c, 0x00}, // 99 c
{0x06, 0x06, 0x3e, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 100 d
{0x00, 0x00, 0x3c, 0x66, 0x7e, 0x60, 0x3c, 0x00}, // 101 e
{0x1c, 0x36, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x00}, // 102 f
{0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 103 g
{0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 104 h
{0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 105 i
{0x06, 0x00, 0x1e, 0x06, 0x06, 0x06, 0x66, 0x3c}, // 106 j
{0x60, 0x60, 0x66, 0x6c, 0x78, 0x6c, 0x66, 0x00}, // 107 k
{0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 108 l
{0x00, 0x00, 0x66, 0x7f, 0x5b, 0x49, 0x49, 0x00}, // 109 m
{0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 110 n
{0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00}, // 111 o
{0x00, 0x00, 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60}, // 112 p
{0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x06}, // 113 q
{0x00, 0x00, 0x7c, 0x66, 0x60, 0x60, 0x60, 0x00}, // 114 r
{0x00, 0x00, 0x3e, 0x60, 0x3c, 0x06, 0x7c, 0x00}, // 115 s
{0x30, 0x30, 0x7c, 0x30, 0x30, 0x36, 0x1c, 0x00}, // 116 t
{0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 117 u
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00}, // 118 v
{0x00, 0x00, 0x63, 0x6b, 0x7f, 0x3e, 0x1c, 0x00}, // 119 w
{0x00, 0x00, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x00}, // 120 x
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 121 y
{0x00, 0x00, 0x7e, 0x0c, 0x18, 0x30, 0x7e, 0x00}, // 122 z
{0x0c, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0c, 0x00}, // 123 {
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // 124 |
{0x30, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x30, 0x00}, // 125 }
{0x3b, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // 126 ~
};
}

450
Game.cpp

@ -0,0 +1,450 @@
#include "Game.hpp"
#include <algorithm>
#include <chrono>
#include <iostream>
// SRS Wall-Kick translation vectors (x, y) for 3x3 shapes (T, J, L, S, Z)
// Direction is either Clockwise (CW) or Counter-Clockwise (CCW).
// Format: KICKS_3x3[CW/CCW][StartRot][TestIndex]
static const Point KICKS_3x3_CW[4][5] = {
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 0 -> 1
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 2
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 2 -> 3
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 0
};
static const Point KICKS_3x3_CCW[4][5] = {
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 0 -> 3
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 0
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 2 -> 1
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 2
};
// SRS Wall-Kick translation vectors (x, y) for I piece (4x4)
static const Point KICKS_I_CW[4][5] = {
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} }, // 0 -> 1
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 1 -> 2
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 2 -> 3
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} } // 3 -> 0
};
static const Point KICKS_I_CCW[4][5] = {
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 0 -> 3
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 1 -> 0
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} }, // 2 -> 1
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} } // 3 -> 2
};
Game::Game() {
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
mRng.seed(seed);
init();
}
void Game::init() {
mState = GameState::START;
reset();
}
void Game::reset() {
// Clear grid
for (int y = 0; y < BOARD_HEIGHT; ++y) {
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[y][x] = PieceType::NONE;
}
}
mScore = 0;
mLinesCleared = 0;
mLevel = 1;
mCombo = -1;
mHoldType = PieceType::NONE;
mCanHold = true;
mFallTimer = 0.0f;
mLockTimer = 0.0f;
mIsLocking = false;
mShakeIntensity = 0.0f;
mBag.clear();
mNextQueue.clear();
// Populate the queue with initial pieces
fillBag();
for (int i = 0; i < 4; ++i) {
mNextQueue.push_back(popNextPiece());
}
mActiveType = PieceType::NONE;
spawnPiece();
}
void Game::fillBag() {
std::vector<PieceType> pieces = {
PieceType::I, PieceType::O, PieceType::T, PieceType::S, PieceType::Z, PieceType::J, PieceType::L
};
std::shuffle(pieces.begin(), pieces.end(), mRng);
mBag.insert(mBag.end(), pieces.begin(), pieces.end());
}
PieceType Game::popNextPiece() {
if (mBag.empty()) {
fillBag();
}
PieceType next = mBag.back();
mBag.pop_back();
return next;
}
void Game::spawnPiece() {
mActiveType = mNextQueue.front();
mNextQueue.erase(mNextQueue.begin());
mNextQueue.push_back(popNextPiece());
mActiveRot = 0;
// Spawn at top center
mActiveX = BOARD_WIDTH / 2 - 1;
mActiveY = 0;
mCanHold = true;
mIsLocking = false;
mLockTimer = 0.0f;
// Check game over right at spawn
if (fits(mActiveType, mActiveRot, mActiveX, mActiveY)) {
std::cout << "[DEBUG] Game Over triggered on spawn of piece type " << static_cast<int>(mActiveType) << std::endl;
mState = GameState::GAME_OVER;
flagGameOverSFX = true;
} else {
std::cout << "[DEBUG] Spawned piece type " << static_cast<int>(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl;
}
}
std::vector<Point> Game::getActiveCells() const {
std::vector<Point> cells;
if (mActiveType == PieceType::NONE) return cells;
int typeIdx = static_cast<int>(mActiveType);
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][mActiveRot][i];
cells.push_back({mActiveX + p.x, mActiveY + p.y});
}
return cells;
}
std::vector<Point> Game::getGhostCells() const {
std::vector<Point> cells = getActiveCells();
if (cells.empty()) return cells;
int dy = 0;
while (!checkCollision(cells, 0, dy + 1)) {
dy++;
}
for (auto& p : cells) {
p.y += dy;
}
return cells;
}
bool Game::checkCollision(const std::vector<Point>& cells, int dx, int dy) const {
for (const auto& p : cells) {
int nx = p.x + dx;
int ny = p.y + dy;
// Bounds check
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) {
return true;
}
// Top cap check (allow pieces to scroll off top grid temporarily)
if (ny < 0) continue;
if (mBoard[ny][nx] != PieceType::NONE) {
return true;
}
}
return false;
}
bool Game::fits(PieceType type, int rot, int cx, int cy) const {
int typeIdx = static_cast<int>(type);
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][rot][i];
int nx = cx + p.x;
int ny = cy + p.y;
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) {
return true;
}
if (ny < 0) continue;
if (mBoard[ny][nx] != PieceType::NONE) {
return true;
}
}
return false;
}
bool Game::moveLeft() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, -1, 0)) {
mActiveX--;
flagMoveSFX = true;
// Slide kick resets lock timer
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
return false;
}
bool Game::moveRight() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, 1, 0)) {
mActiveX++;
flagMoveSFX = true;
// Slide kick resets lock timer
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
return false;
}
bool Game::rotate(int dir) {
if (mState != GameState::PLAYING || mActiveType == PieceType::NONE) return false;
if (mActiveType == PieceType::O) return false; // O piece does not rotate
int nextRot = (mActiveRot + dir + 4) % 4;
// Get wall kick offset trials
const Point(*kicks)[5] = nullptr;
if (mActiveType == PieceType::I) {
kicks = (dir == 1) ? KICKS_I_CW : KICKS_I_CCW;
} else {
kicks = (dir == 1) ? KICKS_3x3_CW : KICKS_3x3_CCW;
}
// Try all 5 Wall-kick tests
for (int t = 0; t < 5; ++t) {
int dx = kicks[mActiveRot][t].x;
int dy = kicks[mActiveRot][t].y;
if (!fits(mActiveType, nextRot, mActiveX + dx, mActiveY + dy)) {
// Apply rotation and kick offset
mActiveRot = nextRot;
mActiveX += dx;
mActiveY += dy;
flagRotateSFX = true;
// Reset lock delay on successful rotation
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
}
return false;
}
void Game::softDrop() {
if (mState != GameState::PLAYING) return;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, 0, 1)) {
mActiveY++;
mScore += 1; // Soft drop score
mFallTimer = 0.0f;
}
}
bool Game::hardDrop() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
int dy = 0;
while (!checkCollision(cells, 0, dy + 1)) {
dy++;
}
mActiveY += dy;
mScore += dy * 2; // Hard drop score
// Force instant lock
mShakeIntensity = 0.15f + (dy * 0.015f); // hard drop screenshake!
lockPiece();
return true;
}
void Game::holdPiece() {
if (mState != GameState::PLAYING || !mCanHold) return;
flagRotateSFX = true; // cool swoosh
PieceType temp = mHoldType;
mHoldType = mActiveType;
if (temp == PieceType::NONE) {
spawnPiece();
} else {
mActiveType = temp;
mActiveRot = 0;
mActiveX = BOARD_WIDTH / 2 - 1;
mActiveY = 0;
mIsLocking = false;
mLockTimer = 0.0f;
}
mCanHold = false;
}
float Game::getFallDelay() const {
// Standard progressive fall timing (seconds per grid step)
switch (mLevel) {
case 1: return 0.85f;
case 2: return 0.72f;
case 3: return 0.60f;
case 4: return 0.48f;
case 5: return 0.38f;
case 6: return 0.30f;
case 7: return 0.22f;
case 8: return 0.16f;
case 9: return 0.11f;
case 10: return 0.08f;
default: return 0.06f; // level 11+
}
}
bool Game::update(float dt) {
if (mState != GameState::PLAYING) return false;
if (mShakeIntensity > 0.0f) {
mShakeIntensity -= dt * 0.8f; // Decay over time
if (mShakeIntensity < 0.0f) mShakeIntensity = 0.0f;
}
mClearedLinesThisTick.clear();
std::vector<Point> cells = getActiveCells();
bool onGround = checkCollision(cells, 0, 1);
if (onGround) {
if (!mIsLocking) {
mIsLocking = true;
mLockTimer = 0.0f;
}
mLockTimer += dt;
// Lock delay threshold of 0.45 seconds
if (mLockTimer >= 0.45f) {
lockPiece();
return true;
}
} else {
mIsLocking = false;
mFallTimer += dt;
float fallDelay = getFallDelay();
if (mFallTimer >= fallDelay) {
mFallTimer -= fallDelay;
mActiveY++;
}
}
return false;
}
void Game::lockPiece() {
std::vector<Point> cells = getActiveCells();
if (cells.empty()) return;
mLastLockedCells = cells;
for (const auto& p : cells) {
if (p.y >= 0 && p.y < BOARD_HEIGHT && p.x >= 0 && p.x < BOARD_WIDTH) {
mBoard[p.y][p.x] = mActiveType;
}
}
flagLandSFX = true;
clearLines();
spawnPiece();
}
void Game::clearLines() {
std::vector<int> fullLines;
for (int y = 0; y < BOARD_HEIGHT; ++y) {
bool full = true;
for (int x = 0; x < BOARD_WIDTH; ++x) {
if (mBoard[y][x] == PieceType::NONE) {
full = false;
break;
}
}
if (full) {
fullLines.push_back(y);
}
}
if (fullLines.empty()) {
mCombo = -1; // reset combo
return;
}
mClearedLinesThisTick = fullLines; // save for particle animations
mCombo++;
// Scoring guideline
int baseScore = 0;
int cleared = fullLines.size();
if (cleared == 1) {
baseScore = 100;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.08f);
} else if (cleared == 2) {
baseScore = 300;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.15f);
} else if (cleared == 3) {
baseScore = 500;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.22f);
} else if (cleared >= 4) {
baseScore = 800;
flagTetrisClearSFX = true; // Boom! Tetris!
mShakeIntensity = std::max(mShakeIntensity, 0.40f); // Screen Rumble!
}
mScore += baseScore * mLevel;
if (mCombo > 0) {
mScore += 50 * mCombo * mLevel;
}
// Erase cleared lines and shift elements down
for (int lineY : fullLines) {
for (int currY = lineY; currY > 0; --currY) {
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[currY][x] = mBoard[currY - 1][x];
}
}
// Top line cleared
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[0][x] = PieceType::NONE;
}
}
mLinesCleared += cleared;
// Level up logic (every 10 lines)
int nextLevel = (mLinesCleared / 10) + 1;
if (nextLevel > mLevel) {
mLevel = nextLevel;
flagLevelUpSFX = true;
}
}

189
Game.hpp

@ -0,0 +1,189 @@
#pragma once
#include <vector>
#include <random>
enum class PieceType {
I = 0, O, T, S, Z, J, L, NONE
};
struct Point {
int x;
int y;
};
// Coordinates for all 7 Tetromino types in their 4 rotation states (0, 1, 2, 3)
// Pivot anchor is (0,0). Screen space has Y-down, X-right.
inline const Point TETROMINO_CELLS[7][4][4] = {
// 0: I Piece
{
{ {-1, 0}, {0, 0}, {1, 0}, {2, 0} }, // rot 0
{ {1, -1}, {1, 0}, {1, 1}, {1, 2} }, // rot 1
{ {-1, 1}, {0, 1}, {1, 1}, {2, 1} }, // rot 2
{ {0, -1}, {0, 0}, {0, 1}, {0, 2} } // rot 3
},
// 1: O Piece
{
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 0
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 1
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 2
{ {0, 0}, {1, 0}, {0, 1}, {1, 1} } // rot 3
},
// 2: T Piece
{
{ {-1, 0}, {0, 0}, {1, 0}, {0, 1} }, // rot 0
{ {0, -1}, {0, 0}, {0, 1}, {-1, 0} }, // rot 1
{ {-1, 0}, {0, 0}, {1, 0}, {0, -1} }, // rot 2
{ {0, -1}, {0, 0}, {0, 1}, {1, 0} } // rot 3
},
// 3: S Piece
{
{ {0, 0}, {1, 0}, {-1, 1}, {0, 1} }, // rot 0
{ {0, -1}, {0, 0}, {1, 0}, {1, 1} }, // rot 1
{ {0, -1}, {1, -1}, {-1, 0}, {0, 0} }, // rot 2
{ {-1, -1}, {-1, 0}, {0, 0}, {0, 1} } // rot 3
},
// 4: Z Piece
{
{ {-1, 0}, {0, 0}, {0, 1}, {1, 1} }, // rot 0
{ {1, -1}, {1, 0}, {0, 0}, {0, 1} }, // rot 1
{ {-1, -1}, {0, -1}, {0, 0}, {1, 0} }, // rot 2
{ {0, -1}, {0, 0}, {-1, 0}, {-1, 1} } // rot 3
},
// 5: J Piece
{
{ {-1, 0}, {0, 0}, {1, 0}, {-1, 1} }, // rot 0
{ {0, -1}, {0, 0}, {0, 1}, {-1, -1} }, // rot 1
{ {-1, 0}, {0, 0}, {1, 0}, {1, -1} }, // rot 2
{ {0, -1}, {0, 0}, {0, 1}, {1, 1} } // rot 3
},
// 6: L Piece
{
{ {-1, 0}, {0, 0}, {1, 0}, {1, 1} }, // rot 0
{ {0, -1}, {0, 0}, {0, 1}, {-1, 1} }, // rot 1
{ {-1, 0}, {0, 0}, {1, 0}, {-1, -1} }, // rot 2
{ {0, -1}, {0, 0}, {0, 1}, {1, -1} } // rot 3
}
};
enum class GameState {
START,
PLAYING,
PAUSED,
GAME_OVER
};
class Game {
public:
static constexpr int BOARD_WIDTH = 10;
static constexpr int BOARD_HEIGHT = 20;
Game();
~Game() = default;
void init();
void reset();
// Core game loop update (handles gravity, lock delay, game states)
// Returns true if lines were cleared in this frame
bool update(float dt);
// Gameplay commands (inputs)
bool moveLeft();
bool moveRight();
bool rotate(int dir); // dir = 1 (CW), -1 (CCW)
void softDrop();
bool hardDrop(); // Returns true if piece locked
void holdPiece();
// Query states
GameState getState() const { return mState; }
void setState(GameState state) { mState = state; }
int getScore() const { return mScore; }
int getLinesCleared() const { return mLinesCleared; }
int getLevel() const { return mLevel; }
int getCombo() const { return mCombo; }
PieceType getCell(int x, int y) const {
if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) return PieceType::NONE;
return mBoard[y][x];
}
PieceType getActivePieceType() const { return mActiveType; }
int getActiveX() const { return mActiveX; }
int getActiveY() const { return mActiveY; }
int getActiveRotation() const { return mActiveRot; }
// Relative cell coordinates of current falling block
std::vector<Point> getActiveCells() const;
// Absolute cell coordinates of where active piece would land (ghost projection)
std::vector<Point> getGhostCells() const;
PieceType getHoldPieceType() const { return mHoldType; }
bool canHold() const { return mCanHold; }
std::vector<PieceType> getNextQueue() const { return mNextQueue; }
// Inter-thread game sound effects triggers
bool flagMoveSFX = false;
bool flagRotateSFX = false;
bool flagLandSFX = false;
bool flagLineClearSFX = false;
bool flagTetrisClearSFX = false;
bool flagLevelUpSFX = false;
bool flagGameOverSFX = false;
// Screen shake parameters
float getScreenShakeIntensity() const { return mShakeIntensity; }
void resetScreenShake() { mShakeIntensity = 0.0f; }
// List of lines cleared on the current tick (for particle animations)
std::vector<int> mClearedLinesThisTick;
std::vector<Point> mLastLockedCells;
private:
GameState mState = GameState::START;
// The grid: index y=0 is top, y=19 is bottom
PieceType mBoard[BOARD_HEIGHT][BOARD_WIDTH];
// Falling piece details
PieceType mActiveType = PieceType::NONE;
int mActiveX = 0;
int mActiveY = 0;
int mActiveRot = 0;
// Hold slot details
PieceType mHoldType = PieceType::NONE;
bool mCanHold = true;
// 7-piece bag randomizer
std::vector<PieceType> mNextQueue;
std::vector<PieceType> mBag;
std::mt19937 mRng;
// Score metrics
int mScore = 0;
int mLinesCleared = 0;
int mLevel = 1;
int mCombo = -1;
// Fall & Lock delay timers
float mFallTimer = 0.0f;
float mLockTimer = 0.0f;
bool mIsLocking = false;
float mShakeIntensity = 0.0f;
float getFallDelay() const;
void spawnPiece();
void fillBag();
PieceType popNextPiece();
bool checkCollision(const std::vector<Point>& cells, int dx, int dy) const;
bool fits(PieceType type, int rot, int cx, int cy) const;
void lockPiece();
void clearLines();
};

28
Makefile

@ -0,0 +1,28 @@
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 `sdl2-config --cflags`
LDFLAGS = `sdl2-config --libs`
TARGET = cybermatris
OBJS = main.o Game.o Synth.o Renderer.o
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS)
main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp
$(CXX) $(CXXFLAGS) -c main.cpp
Game.o: Game.cpp Game.hpp
$(CXX) $(CXXFLAGS) -c Game.cpp
Synth.o: Synth.cpp Synth.hpp
$(CXX) $(CXXFLAGS) -c Synth.cpp
Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp
$(CXX) $(CXXFLAGS) -c Renderer.cpp
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean

109
Makefile.miyoo

@ -0,0 +1,109 @@
# ============================================================
# Makefile.miyoo - Cross-compile CyberMatris per Miyoo Mini Plus
# Toolchain: mini_toolchain-v1.0 (SigmaStar ARM)
# SDL2: sdl2-miyoo (build custom con backend MI GFX/AO)
#
# Usage: make -f Makefile.miyoo
# Deploy: make -f Makefile.miyoo deploy (richiede SSH su 10.0.0.199)
# ============================================================
TOOLCHAIN_ROOT = /home/enne2/dev/mini_toolchain-v1.0/mini
SDL2_ROOT = /home/enne2/dev/sdl2-miyoo
CROSS = $(TOOLCHAIN_ROOT)/bin/arm-linux-gnueabihf-
CXX = $(CROSS)g++
STRIP = $(CROSS)strip
SDL2_INC = $(SDL2_ROOT)/sdl2/include
SDL2_LIB = $(SDL2_ROOT)/sdl2/build/.libs
SYSROOT = $(TOOLCHAIN_ROOT)/arm-buildroot-linux-gnueabihf/sysroot
CXXFLAGS = -std=c++17 -O2 -Wall -Wextra \
-I$(SDL2_INC) \
-I$(SDL2_INC)/SDL2 \
-DSDL_MAIN_HANDLED \
-DMIYOO_BUILD
# Linka SDL2 dinamicamente (il .so Miyoo custom include il backend MI_GFX/MI_AO)
# Le libmi_*.so sono proprietarie SigmaStar: esistono SOLO sul device in /config/lib/
# Vengono risolte a runtime tramite LD_LIBRARY_PATH=.:/config/lib
# Usiamo --allow-shlib-undefined perché il linker host non le vede ma sul device ci sono.
LDFLAGS = -L$(SDL2_LIB) \
-lSDL2 \
-lpthread -lm -ldl -lrt \
-Wl,-rpath,'$$ORIGIN' \
-Wl,--allow-shlib-undefined
TARGET = cybermatris_miyoo
OBJS = main.o Game.o Synth.o Renderer.o
DEPLOY_IP = 10.0.0.199
DEPLOY_USER = root
DEPLOY_PASS =
DEPLOY_DIR = /mnt/SDCARD/Roms/PORTS/Games/CyberMatris
# Runtime libs da copiare nella stessa cartella del binario
RUNTIME_LIBS = \
$(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0 \
$(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0.18.2 \
$(SDL2_ROOT)/sdl2/build/.libs/libEGL.so \
$(SDL2_ROOT)/sdl2/build/.libs/libGLESv2.so \
$(SYSROOT)/usr/lib/libjson-c.so.5 \
$(SYSROOT)/usr/lib/libjson-c.so.5.1.0
.PHONY: all clean deploy
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)
$(STRIP) $@
@echo ""
@echo ">>> Build completata: $(TARGET)"
@echo ">>> Dimensione: $$(du -sh $(TARGET) | cut -f1)"
main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp
$(CXX) $(CXXFLAGS) -c main.cpp
Game.o: Game.cpp Game.hpp Synth.hpp
$(CXX) $(CXXFLAGS) -c Game.cpp
Synth.o: Synth.cpp Synth.hpp
$(CXX) $(CXXFLAGS) -c Synth.cpp
Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp
$(CXX) $(CXXFLAGS) -c Renderer.cpp
clean:
rm -f $(OBJS) $(TARGET)
# -------------------------------------------------------
# Crea il pacchetto di deploy (cartella con binario + libs + launcher)
# -------------------------------------------------------
package: $(TARGET)
@mkdir -p dist_miyoo
@cp $(TARGET) dist_miyoo/
@for lib in $(RUNTIME_LIBS); do \
if [ -f "$$lib" ]; then cp "$$lib" dist_miyoo/; echo " Copiata: $$(basename $$lib)"; \
else echo " WARN: $$lib non trovata, skip"; fi; \
done
@cp launch_miyoo.sh dist_miyoo/ 2>/dev/null || true
@echo ""
@echo ">>> Pacchetto pronto in ./dist_miyoo/"
@ls -lh dist_miyoo/
# -------------------------------------------------------
# Deploy via SSH + sshpass (password vuota)
# -------------------------------------------------------
deploy: package
@echo ">>> Deploy su $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)"
sshpass -p '$(DEPLOY_PASS)' ssh -o StrictHostKeyChecking=no \
$(DEPLOY_USER)@$(DEPLOY_IP) "mkdir -p $(DEPLOY_DIR)"
sshpass -p '$(DEPLOY_PASS)' scp -o StrictHostKeyChecking=no \
dist_miyoo/* $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)/
@echo ">>> Deploy completato!"
@echo ""
@echo ">>> Per lanciare sul device:"
@echo " ssh root@$(DEPLOY_IP)"
@echo " kill -STOP \`pidof MainUI\`"
@echo " cd $(DEPLOY_DIR) && LD_LIBRARY_PATH=.:/config/lib ./cybermatris_miyoo"

845
Renderer.cpp

@ -0,0 +1,845 @@
#include "Renderer.hpp"
#include "Font.hpp"
#include <cmath>
#include <cstdlib>
#include <sstream>
#include <iomanip>
#include <algorithm>
// Playboard grid rendering layout variables
static constexpr int CELL_SIZE = 25;
static constexpr int BOARD_X = 275;
static constexpr int BOARD_Y = 50;
static constexpr int BOARD_W = 10 * CELL_SIZE;
static constexpr int BOARD_H = 20 * CELL_SIZE;
// Sleek neon color constants
static const SDL_Color COLOR_CYAN = {0, 240, 255, 255};
static const SDL_Color COLOR_PINK = {255, 0, 150, 255};
static const SDL_Color COLOR_WHITE = {255, 255, 255, 255};
static const SDL_Color COLOR_ORANGE = {255, 140, 0, 255};
static const SDL_Color COLOR_RED = {245, 0, 50, 255};
// Helper function to draw a single pixel with optional alpha blending onto an RGBA32 surface
static inline void drawPixelSoftware(SDL_Surface* surface, int x, int y, SDL_Color color, bool blend) {
if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) return;
uint32_t* pixels = static_cast<uint32_t*>(surface->pixels);
int pitch = surface->pitch / 4; // 32-bit words per row
uint32_t& dstPixel = pixels[y * pitch + x];
if (!blend || color.a == 255) {
dstPixel = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a);
} else if (color.a > 0) {
Uint8 dstR, dstG, dstB, dstA;
SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA);
float alpha = color.a / 255.0f;
float invAlpha = 1.0f - alpha;
Uint8 outR = static_cast<Uint8>(color.r * alpha + dstR * invAlpha);
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha);
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha);
Uint8 outA = static_cast<Uint8>(color.a + dstA * invAlpha);
dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA);
}
}
// Helper function to fill a rectangle with optional alpha blending on an RGBA32 surface
static void fillRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) {
SDL_Rect clippedRect;
SDL_Rect surfaceRect = {0, 0, surface->w, surface->h};
if (!SDL_IntersectRect(rect, &surfaceRect, &clippedRect)) return;
uint32_t* pixels = static_cast<uint32_t*>(surface->pixels);
int pitch = surface->pitch / 4;
if (!blend || color.a == 255) {
uint32_t mappedColor = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a);
for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) {
uint32_t* row = pixels + y * pitch;
std::fill(row + clippedRect.x, row + clippedRect.x + clippedRect.w, mappedColor);
}
} else if (color.a > 0) {
float alpha = color.a / 255.0f;
float invAlpha = 1.0f - alpha;
for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) {
uint32_t* row = pixels + y * pitch;
for (int x = clippedRect.x; x < clippedRect.x + clippedRect.w; ++x) {
uint32_t& dstPixel = row[x];
Uint8 dstR, dstG, dstB, dstA;
SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA);
Uint8 outR = static_cast<Uint8>(color.r * alpha + dstR * invAlpha);
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha);
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha);
Uint8 outA = static_cast<Uint8>(color.a + dstA * invAlpha);
dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA);
}
}
}
}
// Helper function to draw a rectangle border with optional alpha blending on an RGBA32 surface
static void drawRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) {
// Top border
SDL_Rect top = {rect->x, rect->y, rect->w, 1};
fillRectSoftware(surface, &top, color, blend);
// Bottom border
SDL_Rect bottom = {rect->x, rect->y + rect->h - 1, rect->w, 1};
fillRectSoftware(surface, &bottom, color, blend);
// Left border
SDL_Rect left = {rect->x, rect->y, 1, rect->h};
fillRectSoftware(surface, &left, color, blend);
// Right border
SDL_Rect right = {rect->x + rect->w - 1, rect->y, 1, rect->h};
fillRectSoftware(surface, &right, color, blend);
}
// Helper function to draw a line with optional alpha blending on an RGBA32 surface using Bresenham's algorithm
static void drawLineSoftware(SDL_Surface* surface, int x1, int y1, int x2, int y2, SDL_Color color, bool blend) {
// Handle trivial horizontal/vertical cases with fast fillRectSoftware
if (x1 == x2) {
int minY = std::min(y1, y2);
int maxY = std::max(y1, y2);
SDL_Rect rect = {x1, minY, 1, maxY - minY + 1};
fillRectSoftware(surface, &rect, color, blend);
return;
}
if (y1 == y2) {
int minX = std::min(x1, x2);
int maxX = std::max(x1, x2);
SDL_Rect rect = {minX, y1, maxX - minX + 1, 1};
fillRectSoftware(surface, &rect, color, blend);
return;
}
int dx = std::abs(x2 - x1);
int dy = std::abs(y2 - y1);
int sx = (x1 < x2) ? 1 : -1;
int sy = (y1 < y2) ? 1 : -1;
int err = dx - dy;
while (true) {
drawPixelSoftware(surface, x1, y1, color, blend);
if (x1 == x2 && y1 == y2) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x1 += sx;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
}
}
Renderer::Renderer() {
initStars();
}
Renderer::~Renderer() {
if (mFontTexture != nullptr) {
SDL_DestroyTexture(mFontTexture);
}
if (mFontSurface != nullptr) {
SDL_FreeSurface(mFontSurface);
}
if (mBackbufferSurface != nullptr) {
SDL_FreeSurface(mBackbufferSurface);
}
if (mScaledSurface != nullptr) {
SDL_FreeSurface(mScaledSurface);
}
if (mBackbufferTexture != nullptr) {
SDL_DestroyTexture(mBackbufferTexture);
}
}
void Renderer::initStars() {
mStars.clear();
for (int i = 0; i < 60; ++i) {
Star s;
s.x = static_cast<float>(std::rand() % 800);
s.y = static_cast<float>(std::rand() % 600);
s.speed = 10.0f + static_cast<float>(std::rand() % 30);
s.size = 1.0f + static_cast<float>(std::rand() % 3);
s.alpha = 50.0f + static_cast<float>(std::rand() % 150);
mStars.push_back(s);
}
}
bool Renderer::init(SDL_Renderer* renderer, int targetW, int targetH) {
mRenderer = renderer;
mTargetW = targetW;
mTargetH = targetH;
buildFontTexture();
if (mFontSurface == nullptr) {
return false;
}
// Create software backbuffer surface of 800x600 in RGBA32 format
mBackbufferSurface = SDL_CreateRGBSurfaceWithFormat(0, 800, 600, 32, SDL_PIXELFORMAT_RGBA32);
if (mBackbufferSurface == nullptr) {
return false;
}
#ifdef MIYOO_BUILD
// On Miyoo, the maximum streaming texture size supported by the hardware/driver is 640x480.
// So we always scale the 800x600 backbuffer to a 640x480 surface/texture in RAM,
// and let the GPU/hardware scale the 640x480 texture to the actual window resolution (e.g., 752x560 on V4).
mScaledSurface = SDL_CreateRGBSurfaceWithFormat(0, 640, 480, 32, SDL_PIXELFORMAT_RGBA32);
if (mScaledSurface == nullptr) {
return false;
}
mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, 640, 480);
#else
mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, mTargetW, mTargetH);
#endif
if (mBackbufferTexture == nullptr) {
return false;
}
return true;
}
void Renderer::buildFontTexture() {
// Lay out the 95-char font atlas in two rows to stay within the
// 640px texture width limit imposed by the Miyoo MI_GFX driver.
// Row 0: chars 0..47 (ASCII 32..79), Row 1: chars 48..94 (ASCII 80..126)
static constexpr int CHARS_PER_ROW = 48;
int atlasW = CHARS_PER_ROW * 8; // 384 pixels — fits in 640
int atlasH = 16; // 2 rows of 8 pixels each
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(
0, atlasW, atlasH, 32, SDL_PIXELFORMAT_RGBA32);
if (surface == nullptr) return;
SDL_LockSurface(surface);
uint32_t* pixels = (uint32_t*)surface->pixels;
int pitch = atlasW; // pixels per row (stride = atlasW * 4 bytes, but we use uint32*)
for (int c = 0; c < 95; ++c) {
int row = c / CHARS_PER_ROW;
int col = c % CHARS_PER_ROW;
for (int r = 0; r < 8; ++r) {
uint8_t byte = Font::BITMAP[c][r];
for (int bit = 0; bit < 8; ++bit) {
bool on = (byte >> (7 - bit)) & 1;
int px = col * 8 + bit;
int py = row * 8 + r;
pixels[py * pitch + px] = on ? 0xFFFFFFFF : 0x00000000;
}
}
}
SDL_UnlockSurface(surface);
mFontSurface = surface;
mFontTexture = SDL_CreateTextureFromSurface(mRenderer, mFontSurface);
if (mFontTexture != nullptr) {
SDL_SetTextureBlendMode(mFontTexture, SDL_BLENDMODE_BLEND);
}
}
void Renderer::update(float dt) {
mTime += dt;
// 1. Update Parallax Background Stars
for (auto& s : mStars) {
s.y += s.speed * dt;
if (s.y > 600.0f) {
s.y = 0.0f;
s.x = static_cast<float>(std::rand() % 800);
}
}
// 2. Update Physics Particles (gravitational debris)
for (size_t i = 0; i < mParticles.size();) {
Particle& p = mParticles[i];
p.life -= p.decay * dt;
if (p.life <= 0.0f) {
// Remove dead particles
mParticles[i] = mParticles.back();
mParticles.pop_back();
} else {
// Kinematic updates with gravity pulling down
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 320.0f * dt; // gravity
i++;
}
}
}
SDL_Color Renderer::getPieceColor(PieceType type) {
switch (type) {
case PieceType::I: return {0, 240, 240, 255}; // Vibrant Cyan
case PieceType::O: return {240, 240, 0, 255}; // Vibrant Yellow
case PieceType::T: return {170, 0, 245, 255}; // Neon Purple
case PieceType::S: return {0, 240, 0, 255}; // Neon Green
case PieceType::Z: return {245, 0, 0, 255}; // Neon Red
case PieceType::J: return {0, 100, 245, 255}; // Deep Blue
case PieceType::L: return {245, 130, 0, 255}; // Bright Orange
default: return {0, 0, 0, 0};
}
}
void Renderer::drawBlock(int gridX, int gridY, PieceType type, bool isGhost, float alphaOverride) {
if (type == PieceType::NONE) return;
int rx = BOARD_X + gridX * CELL_SIZE;
int ry = BOARD_Y + gridY * CELL_SIZE;
SDL_Color color = getPieceColor(type);
if (isGhost) {
// Draw pulsing outline for ghost pieces
float pulse = 0.5f + 0.3f * std::sin(mTime * 10.0f);
SDL_Color pulseCol = {color.r, color.g, color.b, static_cast<Uint8>(pulse * 255.0f * alphaOverride)};
SDL_Rect rect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2};
drawRectSoftware(mBackbufferSurface, &rect, pulseCol, true);
return;
}
// Neon beveled crystal shading style
// 1. Draw solid crystal core
SDL_Color coreCol = {color.r, color.g, color.b, static_cast<Uint8>(255 * alphaOverride)};
SDL_Rect coreRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2};
fillRectSoftware(mBackbufferSurface, &coreRect, coreCol, true);
// 2. Draw glossy 3D inner reflection core
SDL_Color lighterColor = {
static_cast<Uint8>(std::min(color.r + 60, 255)),
static_cast<Uint8>(std::min(color.g + 60, 255)),
static_cast<Uint8>(std::min(color.b + 60, 255)),
255
};
SDL_Color reflectionCol = {lighterColor.r, lighterColor.g, lighterColor.b, static_cast<Uint8>(200 * alphaOverride)};
SDL_Rect innerRect = {rx + 3, ry + 3, CELL_SIZE - 6, CELL_SIZE - 6};
fillRectSoftware(mBackbufferSurface, &innerRect, reflectionCol, true);
// 3. Highlight top-left border (specular light sheen)
SDL_Color highlightCol = {255, 255, 255, static_cast<Uint8>(180 * alphaOverride)};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true);
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true);
// 4. Shade bottom-right border (3D shadow depth)
SDL_Color shadowCol = {0, 0, 0, static_cast<Uint8>(130 * alphaOverride)};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
}
void Renderer::drawGrid(const Game& game) {
// Subtle dark blue/gray playboard background
SDL_Color boardCol = {15, 17, 24, 255};
SDL_Rect boardBG = {BOARD_X, BOARD_Y, BOARD_W, BOARD_H};
fillRectSoftware(mBackbufferSurface, &boardBG, boardCol, true);
// Draw grid lines
SDL_Color gridLineCol = {30, 34, 46, 120};
for (int x = 1; x < 10; ++x) {
drawLineSoftware(mBackbufferSurface, BOARD_X + x * CELL_SIZE, BOARD_Y, BOARD_X + x * CELL_SIZE, BOARD_Y + BOARD_H, gridLineCol, true);
}
for (int y = 1; y < 20; ++y) {
drawLineSoftware(mBackbufferSurface, BOARD_X, BOARD_Y + y * CELL_SIZE, BOARD_X + BOARD_W, BOARD_Y + y * CELL_SIZE, gridLineCol, true);
}
// Draw locked blocks on the board
for (int y = 0; y < 20; ++y) {
for (int x = 0; x < 10; ++x) {
PieceType cell = game.getCell(x, y);
if (cell != PieceType::NONE) {
// If a line is cleared, skip drawing it here so it flashes white under the particles
if (std::find(game.mClearedLinesThisTick.begin(), game.mClearedLinesThisTick.end(), y) != game.mClearedLinesThisTick.end()) {
continue;
}
drawBlock(x, y, cell);
}
}
}
// Draw dynamic glowing borders around the play board (cyberpunk look)
float pulse = 0.8f + 0.2f * std::sin(mTime * 4.0f);
SDL_Color borderCol = {0, 150, 255, static_cast<Uint8>(255 * pulse)};
// Outer border outline layers
for (int i = 0; i < 3; ++i) {
SDL_Rect border = {BOARD_X - i, BOARD_Y - i, BOARD_W + i * 2, BOARD_H + i * 2};
drawRectSoftware(mBackbufferSurface, &border, borderCol, true);
}
}
void Renderer::drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center, bool glow) {
if (mFontSurface == nullptr) return;
// Font atlas layout: 48 chars per row, 2 rows of 8px each.
static constexpr int CHARS_PER_ROW = 48;
int totalWidth = text.length() * 8 * scale;
int startX = center ? x - totalWidth / 2 : x;
SDL_SetSurfaceBlendMode(mFontSurface, SDL_BLENDMODE_BLEND);
auto charSrc = [](char c) -> SDL_Rect {
int idx = (c >= 32 && c <= 126) ? (c - 32) : 0;
int row = idx / CHARS_PER_ROW;
int col = idx % CHARS_PER_ROW;
return SDL_Rect{ col * 8, row * 8, 8, 8 };
};
// Optional black text outline to simulate glow/shadow
if (glow) {
SDL_SetSurfaceColorMod(mFontSurface, 0, 0, 0);
SDL_SetSurfaceAlphaMod(mFontSurface, 180);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx == 0 && dy == 0) continue;
int curX = startX;
for (char c : text) {
if (c >= 32 && c <= 126) {
SDL_Rect src = charSrc(c);
SDL_Rect dest = { curX + dx * scale, y + dy * scale, 8 * scale, 8 * scale };
SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest);
}
curX += 8 * scale;
}
}
}
}
// Main text render
SDL_SetSurfaceColorMod(mFontSurface, color.r, color.g, color.b);
SDL_SetSurfaceAlphaMod(mFontSurface, color.a);
int curX = startX;
for (char c : text) {
if (c >= 32 && c <= 126) {
SDL_Rect src = charSrc(c);
SDL_Rect dest = { curX, y, 8 * scale, 8 * scale };
SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest);
}
curX += 8 * scale;
}
}
void Renderer::drawUI(const Game& game) {
// 1. Title
float titlePulse = std::sin(mTime * 3.0f);
SDL_Color dynamicColor = (titlePulse >= 0.0f) ? COLOR_CYAN : COLOR_PINK;
drawText("CYBERMATRIS", 400, 15, 3, dynamicColor, true, true);
// 2. Hold Box Panel (Left Side)
drawText("HOLD", 140, 50, 2, COLOR_CYAN, true, true);
SDL_Color panelBorderCol = {0, 150, 255, 120};
SDL_Rect holdRect = {40, 70, 200, 110};
drawRectSoftware(mBackbufferSurface, &holdRect, panelBorderCol, true);
PieceType holdPiece = game.getHoldPieceType();
if (holdPiece != PieceType::NONE) {
int typeIdx = static_cast<int>(holdPiece);
SDL_Color pColor = getPieceColor(holdPiece);
// Compute offsets to center pieces perfectly
int offX = -12;
int offY = -12;
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][0][i];
int rx = 40 + 100 + offX + p.x * CELL_SIZE;
int ry = 70 + 55 + offY + p.y * CELL_SIZE;
// Draw beveled block
SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255};
SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2};
fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true);
SDL_Color highlightCol = {255, 255, 255, 160};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true);
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true);
SDL_Color shadowCol = {0, 0, 0, 110};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
}
}
// 3. Stats Dashboard Panel (Left Side, below Hold)
drawText("STATS", 140, 205, 2, COLOR_CYAN, true, true);
SDL_Rect statsRect = {40, 225, 200, 325};
drawRectSoftware(mBackbufferSurface, &statsRect, panelBorderCol, true);
// Render text metrics inside dashboard
drawText("SCORE", 140, 245, 1, COLOR_CYAN, true, false);
std::stringstream ssScore;
ssScore << std::setw(6) << std::setfill('0') << game.getScore();
drawText(ssScore.str(), 140, 260, 2, COLOR_ORANGE, true, true);
drawText("LEVEL", 140, 305, 1, COLOR_CYAN, true, false);
drawText(std::to_string(game.getLevel()), 140, 320, 2, COLOR_WHITE, true, true);
drawText("LINES", 140, 365, 1, COLOR_CYAN, true, false);
drawText(std::to_string(game.getLinesCleared()), 140, 380, 2, COLOR_WHITE, true, true);
// Show Combo notifier if positive
if (game.getCombo() > 0) {
float comboPulse = std::sin(mTime * 15.0f) * 0.5f + 0.5f;
SDL_Color cColor = {255, static_cast<Uint8>(100 + 155 * comboPulse), 0, 255};
std::string comboStr = "COMBO X" + std::to_string(game.getCombo() + 1);
drawText(comboStr, 140, 440, 2, cColor, true, true);
drawText("Pulsing!", 140, 470, 1, COLOR_PINK, true, false);
} else {
drawText("CHIPTUNE", 140, 445, 1, COLOR_PINK, true, false);
drawText("SYNTH ACTIVE", 140, 465, 1, COLOR_CYAN, true, false);
drawText("4-CH MONSTER", 140, 485, 1, COLOR_WHITE, true, false);
}
// 4. Next Pieces Queue Box (Right Side)
drawText("NEXT", 660, 50, 2, COLOR_CYAN, true, true);
SDL_Rect nextRect = {560, 70, 200, 305};
drawRectSoftware(mBackbufferSurface, &nextRect, panelBorderCol, true);
// Draw next 3 pieces
auto nextQueue = game.getNextQueue();
for (int n = 0; n < 3; ++n) {
if (n >= (int)nextQueue.size()) break;
PieceType nPiece = nextQueue[n];
int typeIdx = static_cast<int>(nPiece);
SDL_Color pColor = getPieceColor(nPiece);
int offX = -12;
int offY = -12;
// Render mini blocks for each next queue element in slots
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][0][i];
int rx = 560 + 100 + offX + p.x * CELL_SIZE;
int ry = 70 + 60 + n * 95 + offY + p.y * CELL_SIZE;
SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255};
SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2};
fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true);
SDL_Color highlightCol = {255, 255, 255, 160};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true);
drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true);
SDL_Color shadowCol = {0, 0, 0, 110};
drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true);
}
}
}
void Renderer::drawVisualizer(Synth& synth, int x, int y, int w, int h) {
// 1. Draw outer pane container
SDL_Color containerCol = {0, 150, 255, 120};
SDL_Rect container = {x, y, w, h};
drawRectSoftware(mBackbufferSurface, &container, containerCol, true);
// Subtle Visualizer label
drawText("AUDIO OSCILLOSCOPE", x + w / 2, y + 10, 1, {0, 240, 255, 255}, true, false);
// Fetch the live stereo buffer copies
auto buffer = synth.getVisualizerBuffer();
if (buffer.empty()) return;
int halfH = h / 2;
int centerY = y + halfH + 10;
int dataW = w - 20;
int dataX = x + 10;
// 2. Draw 12 spectrum equalizer bar segments in background
int numBars = 12;
int barW = (dataW - (numBars - 1) * 3) / numBars;
int samplesPerBar = buffer.size() / numBars;
for (int b = 0; b < numBars; ++b) {
// Calculate dynamic energy (Root Mean Square average amplitude) of segment
float rmsSum = 0.0f;
for (int s = 0; s < samplesPerBar; ++s) {
float val = buffer[b * samplesPerBar + s];
rmsSum += val * val;
}
float rms = std::sqrt(rmsSum / samplesPerBar);
// Smooth scaling factor
float barHeight = rms * (h - 60) * 2.8f;
barHeight = std::clamp(barHeight, 2.0f, static_cast<float>(h - 50));
// Pulsing color cycle: fades from hot purple to high green
SDL_Color barCol = {
static_cast<Uint8>(std::min(rms * 900.0f, 255.0f)),
static_cast<Uint8>(180 - std::min(rms * 300.0f, 180.0f)),
245,
70
};
SDL_Rect barRect = {
dataX + b * (barW + 3),
y + h - 15 - static_cast<int>(barHeight),
barW,
static_cast<int>(barHeight)
};
fillRectSoftware(mBackbufferSurface, &barRect, barCol, true);
}
// 3. Draw neon green oscilloscope line connecting points
SDL_Color waveCol = {0, 255, 120, 255};
int prevX = dataX;
int prevY = centerY;
int step = buffer.size() / dataW;
if (step <= 0) step = 1;
for (int i = 0; i < dataW; ++i) {
int idx = i * step;
if (idx >= (int)buffer.size()) break;
float sample = buffer[idx];
int px = dataX + i;
// Amplify wave height
int py = centerY + static_cast<int>(sample * halfH * 1.5f);
py = std::clamp(py, y + 25, y + h - 10);
if (i > 0) {
drawLineSoftware(mBackbufferSurface, prevX, prevY, px, py, waveCol, true);
}
prevX = px;
prevY = py;
}
}
void Renderer::spawnLineClearParticles(const Game& game) {
// When lines clear, shatter blocks into glowing spark bursts!
for (int y : game.mClearedLinesThisTick) {
// For every block across the line
for (int x = 0; x < 10; ++x) {
PieceType pType = game.getCell(x, y);
if (pType == PieceType::NONE) pType = PieceType::I; // fallback
SDL_Color color = getPieceColor(pType);
int rx = BOARD_X + x * CELL_SIZE + CELL_SIZE / 2;
int ry = BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2;
// Spawn 5 explosive sparks per block
for (int p = 0; p < 5; ++p) {
Particle part;
part.x = static_cast<float>(rx);
part.y = static_cast<float>(ry);
// Random explosive velocity vectors
float angle = static_cast<float>(std::rand() % 360) * (3.14159f / 180.0f);
float speed = 50.0f + static_cast<float>(std::rand() % 220);
part.vx = std::cos(angle) * speed;
part.vy = std::sin(angle) * speed - 50.0f; // slight upwards blast bias
part.life = 1.0f;
part.decay = 1.2f + static_cast<float>(std::rand() % 10) * 0.1f; // decay ~0.8s
part.size = 2.0f + static_cast<float>(std::rand() % 4);
part.color = color;
mParticles.push_back(part);
}
}
}
}
void Renderer::spawnLandDustParticles(int gridX, int gridY, PieceType type) {
if (type == PieceType::NONE) return;
SDL_Color color = {230, 235, 255, 200}; // light grayish white puff
int rx = BOARD_X + gridX * CELL_SIZE + CELL_SIZE / 2;
int ry = BOARD_Y + (gridY + 1) * CELL_SIZE; // spawn right at contact base
// Spawn 8 tiny dust puff particles drifting left and right
for (int i = 0; i < 8; ++i) {
Particle p;
p.x = static_cast<float>(rx + (std::rand() % CELL_SIZE) - CELL_SIZE / 2);
p.y = static_cast<float>(ry - 2);
p.vx = static_cast<float>((std::rand() % 100) - 50); // drift sideways
p.vy = -10.0f - static_cast<float>(std::rand() % 30); // slight upwards float
p.life = 1.0f;
p.decay = 2.0f + static_cast<float>(std::rand() % 10) * 0.2f; // quick decay ~0.4s
p.size = 2.0f + static_cast<float>(std::rand() % 3);
p.color = color;
mParticles.push_back(p);
}
}
void Renderer::render(const Game& game, Synth& synth) {
if (mBackbufferSurface == nullptr) return;
// 1. Clear Screen with deep cosmic space black
SDL_Color clearCol = {10, 11, 16, 255};
SDL_Rect bgRect = {0, 0, 800, 600};
fillRectSoftware(mBackbufferSurface, &bgRect, clearCol, false);
// 2. Render drifting parallax background stars
for (const auto& s : mStars) {
SDL_Color starCol = {255, 255, 255, static_cast<Uint8>(s.alpha)};
SDL_Rect starRect = {
static_cast<int>(s.x),
static_cast<int>(s.y),
static_cast<int>(s.size),
static_cast<int>(s.size)
};
fillRectSoftware(mBackbufferSurface, &starRect, starCol, true);
}
// Apply decayed screenshake camera offsets if any
int camDX = 0;
int camDY = 0;
mShakeIntensity = game.getScreenShakeIntensity();
if (mShakeIntensity > 0.01f) {
camDX = static_cast<int>(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f);
camDY = static_cast<int>(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f);
}
// 3. Render the dynamic cyberpunk grid and locked board blocks
drawGrid(game);
// 4. Render Active Tetromino (glowing crystal style)
PieceType activeType = game.getActivePieceType();
if (activeType != PieceType::NONE) {
// A. Draw Ghost projection piece first (pulsing neon wireframe)
auto ghostCells = game.getGhostCells();
for (const auto& p : ghostCells) {
drawBlock(p.x, p.y, activeType, true);
}
// B. Draw Active Piece blocks
auto activeCells = game.getActiveCells();
for (const auto& p : activeCells) {
drawBlock(p.x, p.y, activeType);
}
}
// 5. Render sidebars, score gauge, next and hold compartments
drawUI(game);
// 6. Render real-time audio visualizer oscilloscope panel
drawVisualizer(synth, 560, 390, 200, 160);
// 7. Render physics particles (debris sparks)
for (const auto& p : mParticles) {
SDL_Color pCol = {p.color.r, p.color.g, p.color.b, static_cast<Uint8>(p.life * 255.0f)};
SDL_Rect pRect = {
static_cast<int>(p.x - p.size / 2),
static_cast<int>(p.y - p.size / 2),
static_cast<int>(p.size),
static_cast<int>(p.size)
};
fillRectSoftware(mBackbufferSurface, &pRect, pCol, true);
}
// 8. Render Game Over / Start Banner overlay
if (game.getState() == GameState::GAME_OVER) {
// Translucent overlay shroud
SDL_Color overlayCol = {10, 10, 15, 200};
SDL_Rect overlay = {0, 0, 800, 600};
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true);
float flash = std::sin(mTime * 6.0f) * 0.5f + 0.5f;
SDL_Color flashRed = {255, static_cast<Uint8>(flash * 100), static_cast<Uint8>(flash * 50), 255};
drawText("GAME OVER", 400, 220, 4, flashRed, true, true);
std::stringstream ssFinalScore;
ssFinalScore << "FINAL SCORE " << game.getScore();
drawText(ssFinalScore.str(), 400, 280, 2, COLOR_CYAN, true, false);
drawText("PRESS R TO RESTART", 400, 350, 2, COLOR_WHITE, true, true);
drawText("OR ESC TO QUIT", 400, 390, 1, {180, 180, 200, 255}, true, false);
} else if (game.getState() == GameState::START) {
SDL_Color overlayCol = {8, 9, 13, 230};
SDL_Rect overlay = {0, 0, 800, 600};
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true);
// Blinking start cue
float blink = std::sin(mTime * 5.0f) * 0.5f + 0.5f;
SDL_Color cueColor = {static_cast<Uint8>(180 + 75 * blink), static_cast<Uint8>(200 + 55 * blink), 255, 255};
drawText("CYBERMATRIS", 400, 180, 5, COLOR_PINK, true, true);
drawText("RETROWAVE PROCEDURAL TETRIS", 400, 240, 1, COLOR_CYAN, true, false);
// Game features overview
drawText("PROCEDURAL CHIPTUNE MUSIC", 400, 300, 1, COLOR_WHITE, true, false);
drawText("NEON PARTICLE SHATTER EFFECTS", 400, 320, 1, COLOR_WHITE, true, false);
drawText("REAL-TIME AUDIO OSCILLOSCOPE", 400, 340, 1, COLOR_WHITE, true, false);
drawText("PRESS ENTER TO START", 400, 420, 2, cueColor, true, true);
// Controls list
drawText("CONTROLS", 400, 475, 1, COLOR_CYAN, true, false);
drawText("LEFT RIGHT MOVEMENT DOWN SOFT DROP SPACE HARD DROP", 400, 495, 1, {170, 180, 200, 255}, true, false);
drawText("UP X ROTATE CLOCKWISE Z ROTATE COUNTER C HOLD PIECE", 400, 515, 1, {170, 180, 200, 255}, true, false);
drawText("P PAUSE GAME R RESTART GAME", 400, 535, 1, {170, 180, 200, 255}, true, false);
drawText("CREATED BY MATTEO BENEDETTO (ENNE2)", 400, 575, 1, {255, 200, 0, 255}, true, false);
} else if (game.getState() == GameState::PAUSED) {
SDL_Color overlayCol = {10, 11, 16, 150};
SDL_Rect overlay = {0, 0, 800, 600};
fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true);
float flash = std::sin(mTime * 4.0f) * 0.5f + 0.5f;
SDL_Color flashCyan = {static_cast<Uint8>(flash * 100), 220, 255, 255};
drawText("PAUSED", 400, 260, 4, flashCyan, true, true);
drawText("PRESS P TO RESUME", 400, 310, 2, COLOR_WHITE, true, false);
}
// 9. Upload backbuffer surface to streaming texture on GPU
#ifdef MIYOO_BUILD
// Scale the 800x600 software backbuffer to target screen size in software RAM
SDL_BlitScaled(mBackbufferSurface, nullptr, mScaledSurface, nullptr);
SDL_UpdateTexture(mBackbufferTexture, nullptr, mScaledSurface->pixels, mScaledSurface->pitch);
#else
SDL_UpdateTexture(mBackbufferTexture, nullptr, mBackbufferSurface->pixels, mBackbufferSurface->pitch);
#endif
// Clear the hardware renderer (Miyoo requires this before rendering copy)
SDL_RenderClear(mRenderer);
// Draw the texture containing our fully rendered software scene
// If screenshake is active, apply the shake offset directly to the destination rectangle!
#ifdef MIYOO_BUILD
SDL_Rect destRect = {
static_cast<int>(camDX * static_cast<float>(mTargetW) / 800.0f),
static_cast<int>(camDY * static_cast<float>(mTargetH) / 600.0f),
mTargetW,
mTargetH
};
#else
SDL_Rect destRect = {
camDX,
camDY,
mTargetW,
mTargetH
};
#endif
SDL_RenderCopy(mRenderer, mBackbufferTexture, nullptr, &destRect);
// Present the frame!
SDL_RenderPresent(mRenderer);
}
void Renderer::takeScreenshot(const std::string& filename) {
if (mBackbufferSurface == nullptr) return;
SDL_SaveBMP(mBackbufferSurface, filename.c_str());
}

74
Renderer.hpp

@ -0,0 +1,74 @@
#pragma once
#include <SDL2/SDL.h>
#include <vector>
#include <string>
#include "Game.hpp"
#include "Synth.hpp"
struct Particle {
float x = 0.0f;
float y = 0.0f;
float vx = 0.0f;
float vy = 0.0f;
float life = 1.0f; // 1.0 down to 0.0
float decay = 1.0f; // decay rate per second
float size = 4.0f;
SDL_Color color = {255, 255, 255, 255};
};
struct Star {
float x = 0.0f;
float y = 0.0f;
float speed = 0.0f;
float size = 0.0f;
float alpha = 0.0f;
};
class Renderer {
public:
Renderer();
~Renderer();
bool init(SDL_Renderer* renderer, int targetW, int targetH);
void update(float dt);
// Master rendering routine
void render(const Game& game, Synth& synth);
// Spawn effects
void spawnLineClearParticles(const Game& game);
void spawnLandDustParticles(int gridX, int gridY, PieceType type);
// Screenshot API
void takeScreenshot(const std::string& filename);
private:
SDL_Renderer* mRenderer = nullptr;
SDL_Texture* mFontTexture = nullptr;
SDL_Surface* mFontSurface = nullptr;
SDL_Surface* mBackbufferSurface = nullptr;
SDL_Surface* mScaledSurface = nullptr;
SDL_Texture* mBackbufferTexture = nullptr;
int mTargetW = 800;
int mTargetH = 600;
float mTime = 0.0f;
float mShakeIntensity = 0.0f;
// Drifting starfield
std::vector<Star> mStars;
std::vector<Particle> mParticles;
void initStars();
void buildFontTexture();
// Drawing helpers
void drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center = false, bool glow = false);
void drawBlock(int gridX, int gridY, PieceType type, bool isGhost = false, float alphaOverride = 1.0f);
void drawGrid(const Game& game);
void drawUI(const Game& game);
void drawVisualizer(Synth& synth, int x, int y, int w, int h);
// Helpers
SDL_Color getPieceColor(PieceType type);
};

802
Synth.cpp

@ -0,0 +1,802 @@
#include "Synth.hpp"
#include <cmath>
#include <cstdlib>
#include <algorithm>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Midi note number to frequency conversion helper
static inline float midiToFreq(int note) {
if (note <= 0) return 0.0f;
return 440.0f * std::pow(2.0f, (note - 69.0f) / 12.0f);
}
Synth::Synth() {
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) {
mVisBuffer[i] = 0.0f;
}
initSequencer();
}
Synth::~Synth() {
if (mAudioDevice > 0) {
SDL_CloseAudioDevice(mAudioDevice);
}
}
bool Synth::init() {
SDL_AudioSpec wanted, obtained;
SDL_zero(wanted);
wanted.freq = 44100;
wanted.format = AUDIO_S16SYS; // Signed 16-bit, native endian
wanted.channels = 2; // Stereo
#ifdef MIYOO_BUILD
wanted.samples = 1024; // Larger buffer size to satisfy Miyoo MI_AO requirements
#else
wanted.samples = 256; // Ultra low latency
#endif
wanted.callback = &Synth::audioCallbackWrapper;
wanted.userdata = this;
mAudioDevice = SDL_OpenAudioDevice(nullptr, 0, &wanted, &obtained, 0);
if (mAudioDevice == 0) {
return false;
}
// Start playing silence initially
SDL_PauseAudioDevice(mAudioDevice, 0);
return true;
}
void Synth::playBGM(bool play) {
std::lock_guard<std::mutex> lock(mMutex);
mBGMEnabled = play;
if (!play) {
// Stop music voices (0, 1, 2, 3)
for (int i = 0; i < 4; ++i) {
mVoices[i].active = false;
}
}
}
void Synth::stopAllSFX() {
std::lock_guard<std::mutex> lock(mMutex);
for (int i = 4; i < NUM_VOICES; ++i) {
mVoices[i].active = false;
}
}
void Synth::shufflePlaylist() {
mPlaylist[0] = 0; // Always anchor with original theme
int variants[] = {1, 2, 3, 4, 5};
std::random_shuffle(std::begin(variants), std::end(variants));
mPlaylist[1] = variants[0];
mPlaylist[2] = variants[1];
mPlaylist[3] = variants[2];
}
void Synth::initSequencer() {
mSecsPerTick = 60.0f / (mBPM * 2.0f);
mTickTimer = 0.0f;
mCurrentTick = 0;
for (int p = 0; p < NUM_PATTERNS; ++p) {
for (int i = 0; i < 64; ++i) {
mPatternLead[p][i] = 0;
mPatternBass[p][i] = 0;
}
for (int b = 0; b < 8; ++b) {
mPatternChords[p][b] = ChordType::AM;
}
}
// PATTERN 0: Original Korobeiniki
mPatternLead[0][0] = 76; mPatternLead[0][1] = 76; mPatternLead[0][2] = 71; mPatternLead[0][3] = 72;
mPatternLead[0][4] = 74; mPatternLead[0][5] = 74; mPatternLead[0][6] = 72; mPatternLead[0][7] = 71;
mPatternLead[0][8] = 69; mPatternLead[0][9] = 69; mPatternLead[0][10] = 69; mPatternLead[0][11] = 72;
mPatternLead[0][12] = 76; mPatternLead[0][13] = 76; mPatternLead[0][14] = 74; mPatternLead[0][15] = 72;
mPatternLead[0][16] = 71; mPatternLead[0][17] = 71; mPatternLead[0][18] = 71; mPatternLead[0][19] = 72;
mPatternLead[0][20] = 74; mPatternLead[0][21] = 74; mPatternLead[0][22] = 76; mPatternLead[0][23] = 76;
mPatternLead[0][24] = 72; mPatternLead[0][25] = 72; mPatternLead[0][26] = 69; mPatternLead[0][27] = 69;
mPatternLead[0][28] = 69;
mPatternLead[0][32] = 74; mPatternLead[0][33] = 74; mPatternLead[0][34] = 74; mPatternLead[0][35] = 77;
mPatternLead[0][36] = 81; mPatternLead[0][37] = 81; mPatternLead[0][38] = 79; mPatternLead[0][39] = 77;
mPatternLead[0][40] = 76; mPatternLead[0][41] = 76; mPatternLead[0][42] = 72; mPatternLead[0][43] = 72;
mPatternLead[0][44] = 76; mPatternLead[0][45] = 76; mPatternLead[0][46] = 74; mPatternLead[0][47] = 72;
mPatternLead[0][48] = 71; mPatternLead[0][49] = 71; mPatternLead[0][50] = 71; mPatternLead[0][51] = 72;
mPatternLead[0][52] = 74; mPatternLead[0][53] = 74; mPatternLead[0][54] = 76; mPatternLead[0][55] = 76;
mPatternLead[0][56] = 72; mPatternLead[0][57] = 72; mPatternLead[0][58] = 69; mPatternLead[0][59] = 69;
mPatternLead[0][60] = 69;
mPatternBass[0][0] = 45; mPatternBass[0][2] = 57; mPatternBass[0][4] = 45; mPatternBass[0][6] = 57;
mPatternBass[0][8] = 45; mPatternBass[0][10] = 57; mPatternBass[0][12] = 45; mPatternBass[0][14] = 57;
mPatternBass[0][16] = 40; mPatternBass[0][18] = 52; mPatternBass[0][20] = 40; mPatternBass[0][22] = 52;
mPatternBass[0][24] = 45; mPatternBass[0][26] = 57; mPatternBass[0][28] = 45; mPatternBass[0][30] = 45;
mPatternBass[0][32] = 50; mPatternBass[0][34] = 62; mPatternBass[0][36] = 50; mPatternBass[0][38] = 62;
mPatternBass[0][40] = 45; mPatternBass[0][42] = 57; mPatternBass[0][44] = 45; mPatternBass[0][46] = 57;
mPatternBass[0][48] = 40; mPatternBass[0][50] = 52; mPatternBass[0][52] = 40; mPatternBass[0][54] = 52;
mPatternBass[0][56] = 45; mPatternBass[0][58] = 57; mPatternBass[0][60] = 45; mPatternBass[0][62] = 45;
mPatternChords[0][0] = ChordType::AM; mPatternChords[0][1] = ChordType::AM;
mPatternChords[0][2] = ChordType::E7; mPatternChords[0][3] = ChordType::AM;
mPatternChords[0][4] = ChordType::DM; mPatternChords[0][5] = ChordType::AM;
mPatternChords[0][6] = ChordType::E7; mPatternChords[0][7] = ChordType::AM;
// PATTERN 1: Fast octave higher walking bass
for(int i=0; i<64; ++i) {
if(mPatternLead[0][i] > 0) mPatternLead[1][i] = mPatternLead[0][i] + 12;
}
mPatternLead[1][32] = 86; mPatternLead[1][33] = 0; mPatternLead[1][34] = 86; mPatternLead[1][35] = 89;
mPatternLead[1][36] = 93; mPatternLead[1][37] = 0; mPatternLead[1][38] = 91; mPatternLead[1][39] = 89;
mPatternLead[1][40] = 88; mPatternLead[1][41] = 0; mPatternLead[1][42] = 84; mPatternLead[1][43] = 84;
mPatternLead[1][44] = 88; mPatternLead[1][45] = 0; mPatternLead[1][46] = 86; mPatternLead[1][47] = 84;
mPatternBass[1][0] = 45; mPatternBass[1][2] = 57; mPatternBass[1][4] = 60; mPatternBass[1][6] = 57;
mPatternBass[1][8] = 45; mPatternBass[1][10] = 57; mPatternBass[1][12] = 60; mPatternBass[1][14] = 57;
mPatternBass[1][16] = 40; mPatternBass[1][18] = 52; mPatternBass[1][20] = 56; mPatternBass[1][22] = 52;
mPatternBass[1][24] = 45; mPatternBass[1][26] = 57; mPatternBass[1][28] = 45; mPatternBass[1][30] = 45;
mPatternBass[1][32] = 50; mPatternBass[1][34] = 62; mPatternBass[1][36] = 65; mPatternBass[1][38] = 62;
mPatternBass[1][40] = 45; mPatternBass[1][42] = 57; mPatternBass[1][44] = 60; mPatternBass[1][46] = 57;
mPatternBass[1][48] = 40; mPatternBass[1][50] = 52; mPatternBass[1][52] = 56; mPatternBass[1][54] = 52;
mPatternBass[1][56] = 45; mPatternBass[1][58] = 57; mPatternBass[1][60] = 45; mPatternBass[1][62] = 45;
for(int i=0; i<8; ++i) mPatternChords[1][i] = mPatternChords[0][i];
// PATTERN 2: Bridge Motif
mPatternLead[2][0] = 81; mPatternLead[2][2] = 84; mPatternLead[2][4] = 89; mPatternLead[2][6] = 84;
mPatternBass[2][0] = 41; mPatternBass[2][2] = 53; mPatternBass[2][4] = 41; mPatternBass[2][6] = 53;
mPatternLead[2][8] = 79; mPatternLead[2][10] = 83; mPatternLead[2][12] = 86; mPatternLead[2][14] = 83;
mPatternBass[2][8] = 43; mPatternBass[2][10] = 55; mPatternBass[2][12] = 43; mPatternBass[2][14] = 55;
mPatternLead[2][16] = 84; mPatternLead[2][18] = 88; mPatternLead[2][20] = 91; mPatternLead[2][22] = 88;
mPatternBass[2][16] = 48; mPatternBass[2][18] = 60; mPatternBass[2][20] = 48; mPatternBass[2][22] = 60;
mPatternLead[2][24] = 81; mPatternLead[2][26] = 84; mPatternLead[2][28] = 88; mPatternLead[2][30] = 84;
mPatternBass[2][24] = 45; mPatternBass[2][26] = 57; mPatternBass[2][28] = 45; mPatternBass[2][30] = 57;
mPatternLead[2][32] = 89; mPatternLead[2][34] = 86; mPatternLead[2][36] = 81; mPatternLead[2][38] = 86;
mPatternBass[2][32] = 50; mPatternBass[2][34] = 62; mPatternBass[2][36] = 50; mPatternBass[2][38] = 62;
mPatternLead[2][40] = 88; mPatternLead[2][42] = 84; mPatternLead[2][44] = 81; mPatternLead[2][46] = 84;
mPatternBass[2][40] = 45; mPatternBass[2][42] = 57; mPatternBass[2][44] = 45; mPatternBass[2][46] = 57;
mPatternLead[2][48] = 88; mPatternLead[2][50] = 86; mPatternLead[2][52] = 83; mPatternLead[2][54] = 80;
mPatternBass[2][48] = 40; mPatternBass[2][50] = 52; mPatternBass[2][52] = 40; mPatternBass[2][54] = 52;
mPatternLead[2][56] = 81; mPatternLead[2][57] = 81; mPatternLead[2][58] = 81;
mPatternBass[2][56] = 45; mPatternBass[2][58] = 45; mPatternBass[2][60] = 45;
mPatternChords[2][0] = ChordType::F; mPatternChords[2][1] = ChordType::G;
mPatternChords[2][2] = ChordType::C; mPatternChords[2][3] = ChordType::AM;
mPatternChords[2][4] = ChordType::DM; mPatternChords[2][5] = ChordType::AM;
mPatternChords[2][6] = ChordType::E7; mPatternChords[2][7] = ChordType::AM;
// PATTERN 3: Driving Resolution
mPatternLead[3][0] = 81; mPatternLead[3][1] = 79; mPatternLead[3][2] = 77; mPatternLead[3][3] = 81;
mPatternLead[3][4] = 84; mPatternLead[3][5] = 81; mPatternLead[3][6] = 77; mPatternLead[3][7] = 81;
mPatternBass[3][0] = 41; mPatternBass[3][2] = 53; mPatternBass[3][4] = 57; mPatternBass[3][6] = 53;
mPatternLead[3][8] = 83; mPatternLead[3][9] = 81; mPatternLead[3][10] = 79; mPatternLead[3][11] = 83;
mPatternLead[3][12] = 86; mPatternLead[3][13] = 83; mPatternLead[3][14] = 79; mPatternLead[3][15] = 83;
mPatternBass[3][8] = 43; mPatternBass[3][10] = 55; mPatternBass[3][12] = 59; mPatternBass[3][14] = 55;
mPatternLead[3][16] = 84; mPatternLead[3][17] = 83; mPatternLead[3][18] = 84; mPatternLead[3][19] = 86;
mPatternLead[3][20] = 88; mPatternLead[3][21] = 86; mPatternLead[3][22] = 84; mPatternLead[3][23] = 88;
mPatternBass[3][16] = 48; mPatternBass[3][18] = 60; mPatternBass[3][20] = 64; mPatternBass[3][22] = 60;
mPatternLead[3][24] = 81; mPatternLead[3][25] = 80; mPatternLead[3][26] = 81; mPatternLead[3][27] = 83;
mPatternLead[3][28] = 84; mPatternLead[3][29] = 83; mPatternLead[3][30] = 81; mPatternLead[3][31] = 84;
mPatternBass[3][24] = 45; mPatternBass[3][26] = 57; mPatternBass[3][28] = 60; mPatternBass[3][30] = 57;
mPatternLead[3][32] = 86; mPatternLead[3][33] = 84; mPatternLead[3][34] = 86; mPatternLead[3][35] = 88;
mPatternLead[3][36] = 89; mPatternLead[3][37] = 88; mPatternLead[3][38] = 86; mPatternLead[3][39] = 89;
mPatternBass[3][32] = 50; mPatternBass[3][34] = 62; mPatternBass[3][36] = 65; mPatternBass[3][38] = 62;
mPatternLead[3][40] = 88; mPatternLead[3][41] = 86; mPatternLead[3][42] = 84; mPatternLead[3][43] = 83;
mPatternLead[3][44] = 81; mPatternLead[3][45] = 84; mPatternLead[3][46] = 88; mPatternLead[3][47] = 84;
mPatternBass[3][40] = 45; mPatternBass[3][42] = 57; mPatternBass[3][44] = 60; mPatternBass[3][46] = 57;
mPatternLead[3][48] = 83; mPatternLead[3][49] = 81; mPatternLead[3][50] = 80; mPatternLead[3][51] = 81;
mPatternLead[3][52] = 83; mPatternLead[3][53] = 84; mPatternLead[3][54] = 86; mPatternLead[3][55] = 83;
mPatternBass[3][48] = 40; mPatternBass[3][50] = 52; mPatternBass[3][52] = 56; mPatternBass[3][54] = 52;
mPatternLead[3][56] = 81; mPatternLead[3][58] = 81; mPatternLead[3][60] = 69;
mPatternBass[3][56] = 45; mPatternBass[3][58] = 45; mPatternBass[3][60] = 45;
for(int i=0; i<8; ++i) mPatternChords[3][i] = mPatternChords[2][i];
// PATTERN 4: Slow menacing / half-time feel of original
for(int i=0; i<8; ++i) mPatternChords[4][i] = mPatternChords[0][i];
mPatternLead[4][0] = 64; mPatternLead[4][4] = 59; mPatternLead[4][8] = 60; mPatternLead[4][12] = 62;
mPatternLead[4][16] = 59; mPatternLead[4][20] = 60; mPatternLead[4][24] = 57; mPatternLead[4][28] = 57;
mPatternLead[4][32] = 62; mPatternLead[4][36] = 65; mPatternLead[4][40] = 64; mPatternLead[4][44] = 60;
mPatternLead[4][48] = 59; mPatternLead[4][52] = 60; mPatternLead[4][56] = 57; mPatternLead[4][60] = 57;
mPatternBass[4][0] = 33; mPatternBass[4][8] = 33;
mPatternBass[4][16] = 28; mPatternBass[4][24] = 33;
mPatternBass[4][32] = 38; mPatternBass[4][40] = 33;
mPatternBass[4][48] = 28; mPatternBass[4][56] = 33;
// PATTERN 5: Frantic Arpeggiator (uses Bridge chords)
for(int i=0; i<8; ++i) mPatternChords[5][i] = mPatternChords[2][i];
for(int b=0; b<8; ++b) {
int root = 0;
switch(mPatternChords[5][b]) {
case ChordType::F: root = 77; break;
case ChordType::G: root = 79; break;
case ChordType::C: root = 84; break;
case ChordType::AM: root = 81; break;
case ChordType::DM: root = 86; break;
case ChordType::E7: root = 80; break;
}
for(int t=0; t<8; ++t) {
if(t%4 == 0) mPatternLead[5][b*8+t] = root;
else if(t%4 == 1) mPatternLead[5][b*8+t] = root+4;
else if(t%4 == 2) mPatternLead[5][b*8+t] = root+7;
else if(t%4 == 3) mPatternLead[5][b*8+t] = root+12;
}
if(mPatternChords[5][b] == ChordType::AM || mPatternChords[5][b] == ChordType::DM) {
mPatternLead[5][b*8+1] -= 1;
mPatternLead[5][b*8+5] -= 1;
}
mPatternBass[5][b*8] = root - 36;
mPatternBass[5][b*8+2] = root - 24;
mPatternBass[5][b*8+4] = root - 36;
mPatternBass[5][b*8+6] = root - 24;
}
shufflePlaylist();
}
void Synth::triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan, float volume) {
if (midiNote == 0) {
if (mVoices[voiceIdx].active && !mVoices[voiceIdx].releasing) {
mVoices[voiceIdx].releasing = true;
mVoices[voiceIdx].releaseAge = 0.0f;
}
return;
}
Voice& v = mVoices[voiceIdx];
v.active = true;
v.wave = wave;
v.frequency = midiToFreq(midiNote);
v.phase = 0.0f;
v.volume = volume;
v.pan = pan;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.sweepDuration = 0.0f;
// Default music envelopes
if (voiceIdx == 0) { // Lead
v.adsr.attackTime = 0.005f;
v.adsr.decayTime = 0.03f;
v.adsr.sustainLevel = 0.6f;
v.adsr.releaseTime = duration * 0.4f;
// Add a soft vibrato for the lead melody
v.vibratoFreq = 6.0f; // 6 Hz LFO
v.vibratoDepth = 0.008f; // ~0.8% frequency wobble
} else if (voiceIdx == 1) { // Harmony
v.adsr.attackTime = 0.01f;
v.adsr.decayTime = 0.05f;
v.adsr.sustainLevel = 0.5f;
v.adsr.releaseTime = duration * 0.3f;
v.vibratoDepth = 0.0f;
} else if (voiceIdx == 2) { // Bass
v.adsr.attackTime = 0.005f;
v.adsr.decayTime = 0.04f;
v.adsr.sustainLevel = 0.7f;
v.adsr.releaseTime = duration * 0.2f;
v.vibratoDepth = 0.0f;
}
}
void Synth::updateSequencer(float dt) {
if (!mBGMEnabled) return;
mTickTimer += dt;
if (mTickTimer >= mSecsPerTick) {
mTickTimer -= mSecsPerTick;
// Advance sequencer tick
mCurrentTick = (mCurrentTick + 1) % 256;
if (mCurrentTick == 0) {
shufflePlaylist();
}
int patternIdx = mPlaylist[mCurrentTick / 64];
int stepInPattern = mCurrentTick % 64;
// 1. Trigger Lead Note (Voice 0)
uint8_t leadNote = mPatternLead[patternIdx][stepInPattern];
if (leadNote > 0) {
triggerMusicNote(0, leadNote, WaveType::SQUARE, mSecsPerTick, 0.35f, 0.12f);
} else {
if (mVoices[0].active && !mVoices[0].releasing) {
mVoices[0].releasing = true;
mVoices[0].releaseAge = 0.0f;
}
}
// 2. Trigger Bass Note (Voice 2)
uint8_t bassNote = mPatternBass[patternIdx][stepInPattern];
if (bassNote > 0) {
triggerMusicNote(2, bassNote, WaveType::TRIANGLE, mSecsPerTick, 0.5f, 0.22f);
} else {
if (mVoices[2].active && !mVoices[2].releasing) {
mVoices[2].releasing = true;
mVoices[2].releaseAge = 0.0f;
}
}
// 3. Trigger Harmony/Chord Arpeggio (Voice 1)
int subTick = mCurrentTick % 4; // arpeggiate at 8th note speed
uint8_t harmonyNote = 0;
ChordType currentChord = mPatternChords[patternIdx][stepInPattern / 8];
if (currentChord == ChordType::F) {
uint8_t arpeggio[] = {53, 57, 60, 57}; // F3, A3, C4, A3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::G) {
uint8_t arpeggio[] = {55, 59, 62, 59}; // G3, B3, D4, B3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::C) {
uint8_t arpeggio[] = {48, 55, 60, 55}; // C3, G3, C4, G3
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::DM) {
uint8_t arpeggio[] = {57, 62, 65, 62}; // A3, D4, F4, D4
harmonyNote = arpeggio[subTick];
} else if (currentChord == ChordType::E7) {
uint8_t arpeggio[] = {56, 59, 64, 59}; // G#3, B3, E4, B3
harmonyNote = arpeggio[subTick];
} else { // AM
uint8_t arpeggio[] = {57, 60, 64, 60}; // A3, C4, E4, C4
harmonyNote = arpeggio[subTick];
}
triggerMusicNote(1, harmonyNote, WaveType::TRIANGLE, mSecsPerTick * 0.9f, 0.65f, 0.08f);
// 4. Trigger Procedural Retro Drums (Voice 3) - Kick, Hat, Snare synthesis!
int drumTick = mCurrentTick % 8;
if (drumTick == 0) { // Kick drum: fast triangle frequency sweep (A1 55Hz to A0 27Hz)
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::TRIANGLE;
v.frequency = 120.0f;
v.startFreq = 120.0f;
v.targetFreq = 30.0f;
v.sweepDuration = 0.09f;
v.sweepAge = 0.0f;
v.phase = 0.0f;
v.volume = 0.45f;
v.pan = 0.5f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.adsr.attackTime = 0.002f;
v.adsr.decayTime = 0.08f;
v.adsr.sustainLevel = 0.1f;
v.adsr.releaseTime = 0.05f;
} else if (drumTick == 4) { // Snare drum: White noise burst + Triangle pop
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::NOISE;
v.frequency = 100.0f; // dummy frequency
v.phase = 0.0f;
v.volume = 0.26f;
v.pan = 0.5f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.sweepDuration = 0.0f;
v.adsr.attackTime = 0.001f;
v.adsr.decayTime = 0.12f;
v.adsr.sustainLevel = 0.05f;
v.adsr.releaseTime = 0.04f;
} else if (drumTick == 2 || drumTick == 6 || drumTick == 7) { // Hi-Hat: very fast noise pop
Voice& v = mVoices[3];
v.active = true;
v.wave = WaveType::NOISE;
v.frequency = 100.0f; // dummy
v.phase = 0.0f;
v.volume = 0.09f;
v.pan = 0.55f;
v.releasing = false;
v.age = 0.0f;
v.releaseAge = 0.0f;
v.vibratoDepth = 0.0f;
v.sweepDuration = 0.0f;
v.adsr.attackTime = 0.001f;
v.adsr.decayTime = 0.025f;
v.adsr.sustainLevel = 0.0f;
v.adsr.releaseTime = 0.015f;
}
}
}
void Synth::triggerSFX(SFXType type) {
std::lock_guard<std::mutex> lock(mMutex);
// Choose one of the dedicated SFX voices (Voices 4, 5, 6, 7) based on sound type
// to prevent overlap truncation.
int voiceIdx = 4;
Voice* v = &mVoices[voiceIdx];
switch (type) {
case SFXType::MOVE: // Voice 4: quick, quiet triangle pluck
v->active = true;
v->wave = WaveType::TRIANGLE;
v->frequency = 180.0f;
v->phase = 0.0f;
v->volume = 0.22f;
v->pan = 0.45f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->sweepDuration = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.001f;
v->adsr.decayTime = 0.04f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.02f;
break;
case SFXType::ROTATE: // Voice 5: quick laser frequency sweep
voiceIdx = 5;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = 330.0f;
v->startFreq = 330.0f;
v->targetFreq = 660.0f;
v->sweepDuration = 0.08f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.07f;
v->pan = 0.55f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.002f;
v->adsr.decayTime = 0.06f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.03f;
break;
case SFXType::LAND: // Voice 4: sub-bass thud (sine decay)
v->active = true;
v->wave = WaveType::SINE;
v->frequency = 90.0f;
v->startFreq = 90.0f;
v->targetFreq = 45.0f;
v->sweepDuration = 0.08f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.35f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.003f;
v->adsr.decayTime = 0.10f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.04f;
break;
case SFXType::LINE_CLEAR: // Voice 6: rapid, bright ascending arpeggio (C5-E5-G5-C6)
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(72); // C5
v->startFreq = midiToFreq(72);
v->targetFreq = midiToFreq(84); // sweeps to C6
v->sweepDuration = 0.22f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.11f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.18f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.10f;
break;
case SFXType::TETRIS_CLEAR: // Voice 6: dramatic multi-freq sweep + noise chord
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(72); // C5
v->startFreq = midiToFreq(72);
v->targetFreq = midiToFreq(96); // sweeps high! C7
v->sweepDuration = 0.40f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.13f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.015f; // extreme vibrato!
v->vibratoFreq = 12.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.35f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.20f;
// Trigger extra white noise burst on SFX voice 7 for explosive impact
v = &mVoices[7];
v->active = true;
v->wave = WaveType::NOISE;
v->phase = 0.0f;
v->volume = 0.22f;
v->pan = 0.5f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->sweepDuration = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.30f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
break;
case SFXType::LEVEL_UP: // Voice 6: triumphant two-note chord fanfares
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(76); // E5
v->startFreq = midiToFreq(76);
v->targetFreq = midiToFreq(88); // Sweeps to E6
v->sweepDuration = 0.30f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.11f;
v->pan = 0.40f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.20f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
// Trigger third note harmony on voice 7
v = &mVoices[7];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = midiToFreq(80); // G#5
v->startFreq = midiToFreq(80);
v->targetFreq = midiToFreq(92); // Sweeps to G#6
v->sweepDuration = 0.30f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.10f;
v->pan = 0.60f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.005f;
v->adsr.decayTime = 0.20f;
v->adsr.sustainLevel = 0.0f;
v->adsr.releaseTime = 0.15f;
break;
case SFXType::GAME_OVER: // Voice 6: sad, sliding down detuned chord
voiceIdx = 6;
v = &mVoices[voiceIdx];
v->active = true;
v->wave = WaveType::TRIANGLE;
v->frequency = 220.0f;
v->startFreq = 220.0f;
v->targetFreq = 80.0f; // sweep down
v->sweepDuration = 0.80f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.35f;
v->pan = 0.40f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.02f; // highly detuned detuning wobble
v->vibratoFreq = 8.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.60f;
v->adsr.sustainLevel = 0.2f; // Game Over chord detunes and sustains nicely!
v->adsr.releaseTime = 0.40f;
// Voice 7 plays detuned counter note
v = &mVoices[7];
v->active = true;
v->wave = WaveType::SQUARE;
v->frequency = 233.0f; // slightly detuned semitone above
v->startFreq = 233.0f;
v->targetFreq = 83.0f; // sweep down
v->sweepDuration = 0.80f;
v->sweepAge = 0.0f;
v->phase = 0.0f;
v->volume = 0.08f;
v->pan = 0.60f;
v->releasing = false;
v->age = 0.0f;
v->releaseAge = 0.0f;
v->vibratoDepth = 0.0f;
v->adsr.attackTime = 0.01f;
v->adsr.decayTime = 0.60f;
v->adsr.sustainLevel = 0.2f;
v->adsr.releaseTime = 0.40f;
break;
}
}
void Synth::updateVoice(Voice& voice, float dt) {
if (!voice.active) return;
if (voice.releasing) {
voice.releaseAge += dt;
if (voice.releaseAge >= voice.adsr.releaseTime) {
voice.active = false;
return;
}
} else {
voice.age += dt;
if (voice.adsr.sustainLevel == 0.0f && voice.age >= voice.adsr.attackTime + voice.adsr.decayTime) {
voice.active = false;
return;
}
}
// Apply frequency sweeps if active
if (voice.sweepDuration > 0.0f) {
voice.sweepAge += dt;
if (voice.sweepAge >= voice.sweepDuration) {
voice.frequency = voice.targetFreq;
voice.sweepDuration = 0.0f;
} else {
float t = voice.sweepAge / voice.sweepDuration;
voice.frequency = voice.startFreq + (voice.targetFreq - voice.startFreq) * t;
}
}
}
float Synth::generateSample(Voice& voice) {
if (!voice.active) return 0.0f;
// Calculate ADSR envelope volume multiplier
float envVolume = 0.0f;
if (voice.releasing) {
if (voice.releaseAge < voice.adsr.releaseTime) {
float progress = voice.releaseAge / voice.adsr.releaseTime;
envVolume = voice.adsr.sustainLevel * (1.0f - progress);
}
} else {
float age = voice.age;
if (age < voice.adsr.attackTime) {
envVolume = age / voice.adsr.attackTime;
} else if (age < voice.adsr.attackTime + voice.adsr.decayTime) {
float progress = (age - voice.adsr.attackTime) / voice.adsr.decayTime;
envVolume = 1.0f - (1.0f - voice.adsr.sustainLevel) * progress;
} else {
envVolume = voice.adsr.sustainLevel;
}
}
if (envVolume <= 0.0f) {
return 0.0f;
}
// Apply vibrato modulation
float modulatedFreq = voice.frequency;
if (voice.vibratoDepth > 0.0f) {
float lfo = std::sin(voice.age * 2.0f * M_PI * voice.vibratoFreq);
modulatedFreq += lfo * voice.vibratoDepth * voice.frequency;
}
// Phase increment
float phaseInc = 2.0f * M_PI * modulatedFreq / 44100.0f;
voice.phase += phaseInc;
if (voice.phase >= 2.0f * M_PI) {
voice.phase -= 2.0f * M_PI;
}
// Oscillator waveform generation
float oscValue = 0.0f;
switch (voice.wave) {
case WaveType::SINE:
oscValue = std::sin(voice.phase);
break;
case WaveType::SQUARE:
// Custom pulse width modulation (50% standard duty cycle, slightly narrow)
oscValue = (std::sin(voice.phase) >= 0.1f ? 1.0f : -1.0f);
break;
case WaveType::TRIANGLE: {
float normalizedPhase = voice.phase / (2.0f * M_PI);
oscValue = 4.0f * std::abs(normalizedPhase - std::floor(normalizedPhase + 0.5f)) - 1.0f;
break;
}
case WaveType::NOISE:
oscValue = (((float)std::rand() / RAND_MAX) * 2.0f - 1.0f);
break;
}
return oscValue * voice.volume * envVolume;
}
void Synth::audioCallback(Uint8* stream, int len) {
int frames = len / 4; // 16-bit signed stereo = 4 bytes per frame
int16_t* out = (int16_t*)stream;
float dt = 1.0f / 44100.0f;
std::lock_guard<std::mutex> lock(mMutex);
for (int f = 0; f < frames; ++f) {
// Advance chiptune sequencer clock
updateSequencer(dt);
float leftMixed = 0.0f;
float rightMixed = 0.0f;
// Render each voice and mix with panning
for (int i = 0; i < NUM_VOICES; ++i) {
if (!mVoices[i].active) continue;
// Render and update phase/envelope
float voiceSample = generateSample(mVoices[i]);
updateVoice(mVoices[i], dt);
// Stereo panning
leftMixed += voiceSample * (1.0f - mVoices[i].pan);
rightMixed += voiceSample * mVoices[i].pan;
}
// Clamp mixed signals to prevent audio clipping
leftMixed = std::clamp(leftMixed, -1.0f, 1.0f);
rightMixed = std::clamp(rightMixed, -1.0f, 1.0f);
// Convert mixed floats back to 16-bit PCM samples
out[f * 2] = (int16_t)(leftMixed * 32767.0f);
out[f * 2 + 1] = (int16_t)(rightMixed * 32767.0f);
// Log sample into the visualizer ring buffer
float avgSample = (leftMixed + rightMixed) * 0.5f;
mVisBuffer[mVisWritePos] = avgSample;
mVisWritePos = (mVisWritePos + 1) % VIS_BUFFER_SIZE;
}
}
void Synth::audioCallbackWrapper(void* userdata, Uint8* stream, int len) {
((Synth*)userdata)->audioCallback(stream, len);
}
std::vector<float> Synth::getVisualizerBuffer() {
std::lock_guard<std::mutex> lock(mMutex);
std::vector<float> res(VIS_BUFFER_SIZE);
// Copy the ring buffer chronologically starting from the current write position
for (int i = 0; i < VIS_BUFFER_SIZE; ++i) {
int idx = (mVisWritePos + i) % VIS_BUFFER_SIZE;
res[i] = mVisBuffer[idx];
}
return res;
}

125
Synth.hpp

@ -0,0 +1,125 @@
#pragma once
#include <SDL2/SDL.h>
#include <mutex>
#include <vector>
enum class WaveType {
SINE,
SQUARE,
TRIANGLE,
NOISE
};
enum class ChordType {
AM,
E7,
DM,
F,
G,
C
};
struct Envelope {
float attackTime = 0.005f; // seconds
float decayTime = 0.05f; // seconds
float sustainLevel = 0.7f; // 0.0 to 1.0
float releaseTime = 0.12f; // seconds
};
struct Voice {
bool active = false;
WaveType wave = WaveType::SQUARE;
float frequency = 0.0f;
float phase = 0.0f;
float volume = 0.12f;
float pan = 0.5f; // 0.0 (left) to 1.0 (right)
// Envelope tracking
Envelope adsr;
float age = 0.0f; // duration active (seconds)
float releaseAge = 0.0f; // duration in release phase (seconds)
bool releasing = false;
// Frequency sweeps (for sound effects)
float startFreq = 0.0f;
float targetFreq = 0.0f;
float sweepDuration = 0.0f;
float sweepAge = 0.0f;
// Vibrato
float vibratoFreq = 0.0f; // LFO speed (Hz)
float vibratoDepth = 0.0f; // pitch variation depth
};
enum class SFXType {
MOVE,
ROTATE,
LAND,
LINE_CLEAR,
TETRIS_CLEAR,
LEVEL_UP,
GAME_OVER
};
class Synth {
public:
Synth();
~Synth();
bool init();
void playBGM(bool play);
void triggerSFX(SFXType type);
void stopAllSFX();
// Thread-safe copy of visualizer buffer for the GPU renderer
std::vector<float> getVisualizerBuffer();
// SDL Audio Callback
void audioCallback(Uint8* stream, int len);
private:
static void audioCallbackWrapper(void* userdata, Uint8* stream, int len);
SDL_AudioDeviceID mAudioDevice = 0;
bool mBGMEnabled = false;
std::mutex mMutex;
// 8 Dedicated voices:
// Voice 0: BGM Lead (Square)
// Voice 1: BGM Harmony / Echo (Pulse)
// Voice 2: BGM Bass (Triangle)
// Voice 3: BGM Drums / Noise (Noise/Triangle)
// Voice 4, 5, 6, 7: Dedicated SFX voices
static constexpr int NUM_VOICES = 8;
Voice mVoices[NUM_VOICES];
// Sequencer state
int mBPM = 138;
float mSecsPerTick = 0.0f; // Seconds per eighth note tick
float mTickTimer = 0.0f;
int mCurrentTick = 0;
// Pattern-based Sequencer
static constexpr int NUM_PATTERNS = 6;
uint8_t mPatternLead[NUM_PATTERNS][64];
uint8_t mPatternBass[NUM_PATTERNS][64];
ChordType mPatternChords[NUM_PATTERNS][8]; // 8 bars per pattern
// Dynamic Playlist (4 patterns per cycle)
int mPlaylist[4];
void shufflePlaylist();
// Visualizer Oscilloscope Ring Buffer
static constexpr int VIS_BUFFER_SIZE = 512;
float mVisBuffer[VIS_BUFFER_SIZE];
int mVisWritePos = 0;
void initSequencer();
void updateSequencer(float dt);
void updateVoice(Voice& voice, float dt);
float generateSample(Voice& voice);
// Helper to start BGM notes
void triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan = 0.5f, float volume = 0.1f);
};

50
launch_miyoo.sh

@ -0,0 +1,50 @@
#!/bin/sh
# Launcher CyberMatris per Miyoo Mini Plus (OnionOS / Stock OS)
GAMEDIR=$(dirname "$0")
LOGFILE="$GAMEDIR/cybermatris.log"
exec > "$LOGFILE" 2>&1
set -x
echo "=== CyberMatris Launch $(date) ==="
# Questo è fondamentale: indica a SDL2 di usare il backend video nativo Miyoo
export SDL_VIDEODRIVER=mmiyoo
unset SDL_RENDER_DRIVER
unset LD_PRELOAD
export HOME=/mnt/SDCARD
export LD_LIBRARY_PATH="$GAMEDIR:/config/lib:/customer/lib:/mnt/SDCARD/.tmp_update/lib/parasyte:/mnt/SDCARD/usr/local/lib:/mnt/SDCARD/usr/lib/arm-linux-gnueabihf"
# Ferma l'audioserver per liberare il device audio hardware (/dev/mi_ao)
killall -9 audioserver 2>/dev/null || true
sleep 1
# Rilevamento dello schermo e impostazione della risoluzione nativa
# Controlliamo la risoluzione corrente PRIMA di modificarla per evitare di forzare
# parametri fuori specifica che congelano il controller dello schermo del Miyoo Mini Plus.
if fbset | grep -q "752"; then
echo "[LAUNCHER] Rilevato schermo Miyoo Mini V4 (752x560)!"
fbset -g 752 560 752 1120 32 2>/dev/null
IS_V4=1
export MIYOO_SCREEN_WIDTH=752
export MIYOO_SCREEN_HEIGHT=560
else
echo "[LAUNCHER] Schermo standard 640x480 (Miyoo Mini Plus / v1/v2/v3)"
fbset -g 640 480 640 960 32 2>/dev/null
IS_V4=0
export MIYOO_SCREEN_WIDTH=640
export MIYOO_SCREEN_HEIGHT=480
fi
cd "$GAMEDIR"
./cybermatris_miyoo
EXIT=$?
# Ripristina sempre la modalità standard 640x480 all'uscita per evitare disallineamenti con MainUI
if [ "$IS_V4" -eq 1 ]; then
echo "[LAUNCHER] Ripristino risoluzione standard all'uscita..."
fbset -g 640 480 640 960 32 2>/dev/null
fi
echo "=== EXIT CODE: $EXIT ==="
exit $EXIT

332
main.cpp

@ -0,0 +1,332 @@
#include <SDL2/SDL.h>
#include <iostream>
#include <chrono>
#ifdef MIYOO_BUILD
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#endif
#include "Game.hpp"
#include "Synth.hpp"
#include "Renderer.hpp"
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
// 1. Initialize SDL2 subsystems
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
return -1;
}
// 2. Create display window
int win_w = 800;
int win_h = 600;
Uint32 win_flags = SDL_WINDOW_SHOWN;
#ifdef MIYOO_BUILD
win_flags |= SDL_WINDOW_FULLSCREEN;
// Default fallback
win_w = 640;
win_h = 480;
// 1. Try environment variables first
const char* env_w = std::getenv("MIYOO_SCREEN_WIDTH");
const char* env_h = std::getenv("MIYOO_SCREEN_HEIGHT");
if (env_w && env_h) {
int parsed_w = std::atoi(env_w);
int parsed_h = std::atoi(env_h);
if (parsed_w > 0 && parsed_h > 0) {
win_w = parsed_w;
win_h = parsed_h;
std::cout << "[INFO] Detected screen resolution from environment: " << win_w << "x" << win_h << std::endl;
}
} else {
// 2. Fallback to /dev/fb0 ioctl
int fd = open("/dev/fb0", O_RDONLY);
if (fd >= 0) {
struct fb_var_screeninfo vinfo;
if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) >= 0) {
if (vinfo.xres > 0 && vinfo.yres > 0) {
win_w = vinfo.xres;
win_h = vinfo.yres;
std::cout << "[INFO] Detected screen resolution from /dev/fb0: " << win_w << "x" << win_h << std::endl;
}
}
close(fd);
} else {
std::cerr << "[WARNING] Failed to open /dev/fb0 for resolution query, using default 640x480" << std::endl;
}
}
#endif
SDL_Window* window = SDL_CreateWindow(
"CyberMatris - Programmatic Retro Tetris",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
win_w, win_h,
win_flags
);
if (window == nullptr) {
std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
SDL_Quit();
return -1;
}
// 3. Create GPU Hardware Accelerated Renderer with VSync
SDL_Renderer* renderer = SDL_CreateRenderer(
window, -1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
#ifdef MIYOO_BUILD
if (renderer != nullptr) {
std::cerr << "[MIYOO] Hardware accelerated renderer OK" << std::endl;
SDL_RenderSetLogicalSize(renderer, 800, 600);
}
#endif
if (renderer == nullptr) {
std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
// 4. Initialize Procedural Audio Synthesizer
Synth synth;
if (!synth.init()) {
std::cerr << "Warning: Could not initialize programmatic audio synthesizer! SDL_Error: " << SDL_GetError() << std::endl;
}
// 5. Initialize Game Logic and Renderer
Game game;
Renderer gameRenderer;
if (!gameRenderer.init(renderer, win_w, win_h)) {
std::cerr << "Failed to build procedural graphics textures!" << std::endl;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
bool quit = false;
SDL_Event event;
// Track frame timing using high-precision performance counters
uint64_t lastTime = SDL_GetPerformanceCounter();
double freq = static_cast<double>(SDL_GetPerformanceFrequency());
float softDropTimer = 0.0f;
// Flush initial queued events (e.g., from OnionOS menu launch/button presses)
std::cout << "[INFO] Flushing initial event queue..." << std::endl;
SDL_Event trashEvent;
int flushedCount = 0;
while (SDL_PollEvent(&trashEvent)) {
flushedCount++;
}
std::cout << "[INFO] Flushed " << flushedCount << " initial events." << std::endl;
// 6. Execution Loop
while (!quit) {
// A. Calculate frame delta-time (dt)
uint64_t currentTime = SDL_GetPerformanceCounter();
float dt = static_cast<float>((currentTime - lastTime) / freq);
lastTime = currentTime;
// Cap dt to prevent massive jumps during window drags or lags
if (dt > 0.1f) dt = 0.1f;
// B. Handle Inputs & Events
while (SDL_PollEvent(&event) != 0) {
if (event.type == SDL_QUIT) {
std::cout << "[EVENT] Received SDL_QUIT signal." << std::endl;
quit = true;
} else if (event.type == SDL_KEYDOWN) {
SDL_Keycode sym = event.key.keysym.sym;
std::cout << "[EVENT] Key Down: " << sym << " (" << SDL_GetKeyName(sym) << ")" << std::endl;
if (sym == SDLK_s) {
gameRenderer.takeScreenshot("screenshot.bmp");
std::cout << "[INFO] Screenshot captured to screenshot.bmp" << std::endl;
}
// START Screen Inputs
if (game.getState() == GameState::START) {
if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) {
std::cout << "[INPUT] Start/Confirm game triggered." << std::endl;
game.setState(GameState::PLAYING);
game.reset();
synth.playBGM(true);
} else if (sym == SDLK_ESCAPE) {
// Cooldown check (1000ms) to prevent immediate exit due to leftover events on boot
Uint32 currentTicks = SDL_GetTicks();
if (currentTicks > 1000) {
std::cout << "[INPUT] Quit game requested via ESCAPE/SELECT (ticks: " << currentTicks << ")" << std::endl;
quit = true;
} else {
std::cout << "[INPUT] Blocked early ESCAPE/SELECT quit request (cooldown ticks: " << currentTicks << ")" << std::endl;
}
}
}
// PLAYING State Inputs
else if (game.getState() == GameState::PLAYING) {
switch (sym) {
case SDLK_LEFT:
game.moveLeft();
break;
case SDLK_RIGHT:
game.moveRight();
break;
case SDLK_UP:
case SDLK_x:
case SDLK_LALT: // Gamepad A (Rotate CW)
game.rotate(1); // Clockwise rotation
break;
case SDLK_z:
case SDLK_LCTRL: // Gamepad B (Rotate CCW)
game.rotate(-1); // Counter-Clockwise rotation
break;
case SDLK_SPACE:
game.hardDrop();
break;
case SDLK_c:
case SDLK_LSHIFT:
game.holdPiece();
break;
case SDLK_p:
game.setState(GameState::PAUSED);
synth.playBGM(false);
break;
case SDLK_r:
synth.stopAllSFX();
game.reset();
synth.playBGM(true);
break;
case SDLK_ESCAPE:
synth.stopAllSFX();
game.setState(GameState::START);
synth.playBGM(false);
break;
default:
break;
}
}
// PAUSED State Inputs
else if (game.getState() == GameState::PAUSED) {
if (sym == SDLK_p) {
game.setState(GameState::PLAYING);
synth.playBGM(true);
} else if (sym == SDLK_ESCAPE) {
synth.stopAllSFX();
game.setState(GameState::START);
synth.playBGM(false);
}
}
// GAME OVER State Inputs
else if (game.getState() == GameState::GAME_OVER) {
if (sym == SDLK_r) {
synth.stopAllSFX();
game.setState(GameState::PLAYING);
game.reset();
synth.playBGM(true);
} else if (sym == SDLK_ESCAPE || sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) {
synth.stopAllSFX();
game.setState(GameState::START);
synth.playBGM(false);
}
}
}
}
// C. Continuous Keyboard Polling for Smooth Soft-Drop sliding
if (game.getState() == GameState::PLAYING) {
const Uint8* keyboardState = SDL_GetKeyboardState(nullptr);
if (keyboardState[SDL_SCANCODE_DOWN]) {
softDropTimer += dt;
if (softDropTimer >= 0.04f) { // Soft drop tick speed (every 40ms)
softDropTimer = 0.0f;
game.softDrop();
}
} else {
softDropTimer = 0.0f;
}
}
// D. Update Game Logic (gravity timing)
bool lineCleared = game.update(dt);
if (lineCleared) {
// Trigger glorious pixel explosion of sparks on cleared rows
gameRenderer.spawnLineClearParticles(game);
}
// E. Bridge Game Events to Synth Sound Triggers and Particle Emitters
if (game.flagMoveSFX) {
synth.triggerSFX(SFXType::MOVE);
game.flagMoveSFX = false;
}
if (game.flagRotateSFX) {
synth.triggerSFX(SFXType::ROTATE);
game.flagRotateSFX = false;
}
if (game.flagLandSFX) {
synth.triggerSFX(SFXType::LAND);
// Spawn subtle white-gray impact smoke clouds at bottom row of contact
auto lockedCells = game.mLastLockedCells;
if (!lockedCells.empty()) {
int lowestY = -100;
for (const auto& p : lockedCells) {
if (p.y > lowestY) lowestY = p.y;
}
for (const auto& p : lockedCells) {
if (p.y == lowestY) {
gameRenderer.spawnLandDustParticles(p.x, p.y, game.getActivePieceType());
}
}
}
game.flagLandSFX = false;
}
if (game.flagLineClearSFX) {
synth.triggerSFX(SFXType::LINE_CLEAR);
game.flagLineClearSFX = false;
}
if (game.flagTetrisClearSFX) {
synth.triggerSFX(SFXType::TETRIS_CLEAR);
game.flagTetrisClearSFX = false;
}
if (game.flagLevelUpSFX) {
synth.triggerSFX(SFXType::LEVEL_UP);
game.flagLevelUpSFX = false;
}
if (game.flagGameOverSFX) {
synth.triggerSFX(SFXType::GAME_OVER);
synth.playBGM(false);
game.flagGameOverSFX = false;
}
// F. Update Parallax Background and Particle physics
gameRenderer.update(dt);
// G. Render active frame (automatically uploads backbuffer and calls SDL_RenderPresent)
gameRenderer.render(game, synth);
// H. Automatic periodic screenshot disabled on Miyoo (FAT32 write-during-render issues)
#ifndef MIYOO_BUILD
static float autoScreenshotTimer = 0.0f;
autoScreenshotTimer += dt;
if (autoScreenshotTimer >= 2.0f) {
autoScreenshotTimer = 0.0f;
gameRenderer.takeScreenshot("screenshot.bmp");
}
#endif
}
// 7. Cleanup and close resources
synth.playBGM(false);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

BIN
screenshot.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Loading…
Cancel
Save