Added challenge mode
This commit is contained in:
@ -1,287 +0,0 @@
|
||||
# Spacetris — Challenge Mode (Asteroids) Implementation Spec for VS Code AI Agent
|
||||
|
||||
> Goal: Implement/extend **CHALLENGE** gameplay in Spacetris (not a separate mode), based on 100 levels with **asteroid** prefilled blocks that must be destroyed to advance.
|
||||
|
||||
---
|
||||
|
||||
## 1) High-level Requirements
|
||||
|
||||
### Modes
|
||||
- Existing mode remains **ENDLESS**.
|
||||
- Add/extend **CHALLENGE** mode with **100 levels**.
|
||||
|
||||
### Core Challenge Loop
|
||||
- Each level starts with **prefilled obstacle blocks** called **Asteroids**.
|
||||
- **Level N** starts with **N asteroids** (placed increasingly higher as level increases).
|
||||
- Player advances to the next level when **ALL asteroids are destroyed**.
|
||||
- Gravity (and optionally lock pressure) increases per level.
|
||||
|
||||
### Asteroid concept
|
||||
Asteroids are special blocks placed into the grid at level start:
|
||||
- They are **not** player-controlled pieces.
|
||||
- They have **types** and **hit points** (how many times they must be cleared via line clears).
|
||||
|
||||
---
|
||||
|
||||
## 2) Asteroid Types & Rules
|
||||
|
||||
Define asteroid types and their behavior:
|
||||
|
||||
### A) Normal Asteroid
|
||||
- `hitsRemaining = 1`
|
||||
- Removed when its row is cleared once.
|
||||
- Never moves (no gravity).
|
||||
|
||||
### B) Armored Asteroid
|
||||
- `hitsRemaining = 2`
|
||||
- On first line clear that includes it: decrement hits and change to cracked visual state.
|
||||
- On second clear: removed.
|
||||
- Never moves (no gravity).
|
||||
|
||||
### C) Falling Asteroid
|
||||
- `hitsRemaining = 2`
|
||||
- On first clear: decrement hits, then **becomes gravity-enabled** (drops until resting).
|
||||
- On second clear: removed.
|
||||
|
||||
### D) Core Asteroid (late levels)
|
||||
- `hitsRemaining = 3`
|
||||
- On each clear: decrement hits and change visual state.
|
||||
- After first hit (or after any hit — choose consistent rule) it becomes gravity-enabled.
|
||||
- On final clear: removed (optionally trigger bigger VFX).
|
||||
|
||||
**Important:** These are all within the same CHALLENGE mode.
|
||||
|
||||
---
|
||||
|
||||
## 3) Level Progression Rules (100 Levels)
|
||||
|
||||
### Asteroid Count
|
||||
- `asteroidsToPlace = level` (Level 1 -> 1 asteroid, Level 2 -> 2 asteroids, …)
|
||||
- Recommendation for implementation safety:
|
||||
- If `level` becomes too large to place comfortably, still place `level` but distribute across more rows and allow overlaps only if empty.
|
||||
- If needed, implement a soft cap for placement attempts (avoid infinite loops). If cannot place all, place as many as possible and log/telemetry.
|
||||
|
||||
### Placement Height / Region
|
||||
- Early levels: place in bottom 2–4 rows.
|
||||
- Mid levels: bottom 6–10 rows.
|
||||
- Late levels: up to ~half board height.
|
||||
- Use a function to define a `minRow..maxRow` region based on `level`.
|
||||
|
||||
Example guidance:
|
||||
- `maxRow = boardHeight - 1`
|
||||
- `minRow = boardHeight - 1 - clamp(2 + level/3, 2, boardHeight/2)`
|
||||
|
||||
### Type Distribution by Level (suggested)
|
||||
- Levels 1–9: Normal only
|
||||
- Levels 10–19: add Armored (small %)
|
||||
- Levels 20–59: add Falling (increasing %)
|
||||
- Levels 60–100: add Core (increasing %)
|
||||
|
||||
---
|
||||
|
||||
## 4) Difficulty Scaling
|
||||
|
||||
### Gravity Speed Scaling
|
||||
Implement per-level gravity scale:
|
||||
- `gravity = baseGravity * (1.0f + level * 0.02f)` (tune)
|
||||
- Or use a curve/table.
|
||||
|
||||
Optional additional scaling:
|
||||
- Reduced lock delay slightly at higher levels
|
||||
- Slightly faster DAS/ARR (if implemented)
|
||||
|
||||
---
|
||||
|
||||
## 5) Win/Lose Conditions
|
||||
|
||||
### Level Completion
|
||||
- Level completes when: `asteroidsRemaining == 0`
|
||||
- Then:
|
||||
- Clear board (or keep board — choose one consistent behavior; recommended: **clear board** for clean progression).
|
||||
- Show short transition (optional).
|
||||
- Load next level, until level 100.
|
||||
- After level 100 completion: show completion screen + stats.
|
||||
|
||||
### Game Over
|
||||
- Standard Tetris game over: stack reaches spawn/top (existing behavior).
|
||||
|
||||
---
|
||||
|
||||
## 6) Rendering / UI Requirements
|
||||
|
||||
### Visual Differentiation
|
||||
Asteroids must be visually distinct from normal tetromino blocks.
|
||||
|
||||
Provide visual states:
|
||||
- Normal: rock texture
|
||||
- Armored: plated / darker
|
||||
- Cracked: visible cracks
|
||||
- Falling: glow rim / hazard stripes
|
||||
- Core: pulsing inner core
|
||||
|
||||
Minimum UI additions (Challenge):
|
||||
- Display `LEVEL: X/100`
|
||||
- Display `ASTEROIDS REMAINING: N` (or an icon counter)
|
||||
|
||||
---
|
||||
|
||||
## 7) Data Structures (C++ Guidance)
|
||||
|
||||
### Cell Representation
|
||||
Each grid cell must store:
|
||||
- Whether occupied
|
||||
- If occupied: is it part of normal tetromino or an asteroid
|
||||
- If asteroid: type + hitsRemaining + gravityEnabled + visualState
|
||||
|
||||
Suggested enums:
|
||||
```cpp
|
||||
enum class CellKind { Empty, Tetromino, Asteroid };
|
||||
|
||||
enum class AsteroidType { Normal, Armored, Falling, Core };
|
||||
|
||||
struct AsteroidCell {
|
||||
AsteroidType type;
|
||||
uint8_t hitsRemaining;
|
||||
bool gravityEnabled;
|
||||
uint8_t visualState; // optional (e.g. 0..n)
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
CellKind kind;
|
||||
// For Tetromino: color/type id
|
||||
// For Asteroid: AsteroidCell data
|
||||
};
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## 8) Line Clear Processing Rules (Important)
|
||||
|
||||
When a line is cleared:
|
||||
|
||||
1. Detect full rows (existing).
|
||||
2. For each cleared row:
|
||||
|
||||
* For each cell:
|
||||
|
||||
* If `kind == Asteroid`:
|
||||
|
||||
* `hitsRemaining--`
|
||||
* If `hitsRemaining == 0`: remove (cell becomes Empty)
|
||||
* Else:
|
||||
|
||||
* Update its visual state (cracked/damaged)
|
||||
* If asteroid type is Falling/Core and rule says it becomes gravity-enabled on first hit:
|
||||
|
||||
* `gravityEnabled = true`
|
||||
3. After clearing rows and collapsing the grid:
|
||||
|
||||
* Apply **asteroid gravity step**:
|
||||
|
||||
* For all gravity-enabled asteroid cells: let them fall until resting.
|
||||
* Ensure stable iteration (bottom-up scan).
|
||||
4. Recount asteroids remaining; if 0 -> level complete.
|
||||
|
||||
**Note:** Decide whether gravity-enabled asteroids fall immediately after the first hit (recommended) and whether they fall as individual cells (recommended) or as clusters (optional later).
|
||||
|
||||
---
|
||||
|
||||
## 9) Asteroid Gravity Algorithm (Simple + Stable)
|
||||
|
||||
Implement a pass:
|
||||
|
||||
* Iterate from bottom-2 to top (bottom-up).
|
||||
* If cell is gravity-enabled asteroid and below is empty:
|
||||
|
||||
* Move down by one
|
||||
* Repeat passes until no movement OR do a while-loop per cell to drop fully.
|
||||
|
||||
Be careful to avoid skipping cells when moving:
|
||||
|
||||
* Use bottom-up iteration and drop-to-bottom logic.
|
||||
|
||||
---
|
||||
|
||||
## 10) Level Generation (Deterministic Option)
|
||||
|
||||
To make challenge reproducible:
|
||||
|
||||
* Use a seed: `seed = baseSeed + level`
|
||||
* Place asteroids with RNG based on level seed.
|
||||
|
||||
Placement constraints:
|
||||
|
||||
* Avoid placing asteroids in the spawn zone/top rows.
|
||||
* Avoid creating impossible scenarios too early:
|
||||
|
||||
* For early levels, ensure at least one vertical shaft exists.
|
||||
|
||||
---
|
||||
|
||||
## 11) Tasks Checklist for AI Agent
|
||||
|
||||
### A) Add Challenge Level System
|
||||
|
||||
* [ ] Add `currentLevel (1..100)` and `mode == CHALLENGE`.
|
||||
* [ ] Add `StartChallengeLevel(level)` function.
|
||||
* [ ] Reset/prepare board state for each level (recommended: clear board).
|
||||
|
||||
### B) Asteroid Placement
|
||||
|
||||
* [ ] Implement `PlaceAsteroids(level)`:
|
||||
|
||||
* Determine region of rows
|
||||
* Choose type distribution
|
||||
* Place `level` asteroid cells into empty spots
|
||||
|
||||
### C) Line Clear Hook
|
||||
|
||||
* [ ] Modify existing line clear code:
|
||||
|
||||
* Apply asteroid hit logic
|
||||
* Update visuals
|
||||
* Enable gravity where required
|
||||
|
||||
### D) Gravity-enabled Asteroids
|
||||
|
||||
* [ ] Implement `ApplyAsteroidGravity()` after line clears and board collapse.
|
||||
|
||||
### E) Level Completion
|
||||
|
||||
* [ ] Track `asteroidsRemaining`.
|
||||
* [ ] When 0: trigger level transition and `StartChallengeLevel(level+1)`.
|
||||
|
||||
### F) UI
|
||||
|
||||
* [ ] Add level & asteroids remaining display.
|
||||
|
||||
---
|
||||
|
||||
## 12) Acceptance Criteria
|
||||
|
||||
* Level 1 spawns exactly 1 asteroid.
|
||||
* Level N spawns N asteroids.
|
||||
* Destroying asteroids requires:
|
||||
|
||||
* Normal: 1 clear
|
||||
* Armored: 2 clears
|
||||
* Falling: 2 clears + becomes gravity-enabled after first hit
|
||||
* Core: 3 clears (+ gravity-enabled rule)
|
||||
* Player advances only when all asteroids are destroyed.
|
||||
* Gravity increases by level and is clearly noticeable by mid-levels.
|
||||
* No infinite loops in placement or gravity.
|
||||
* Challenge works end-to-end through level 100.
|
||||
|
||||
---
|
||||
|
||||
## 13) Notes / Tuning Hooks
|
||||
|
||||
Expose tuning constants:
|
||||
|
||||
* `baseGravity`
|
||||
* `gravityPerLevel`
|
||||
* `minAsteroidRow(level)`
|
||||
* `typeDistribution(level)` weights
|
||||
* `coreGravityOnHit` rule
|
||||
|
||||
---
|
||||
@ -867,6 +867,8 @@ void GameRenderer::renderPlayingState(
|
||||
|
||||
// Draw the game board
|
||||
const auto &board = game->boardRef();
|
||||
const auto &asteroidCells = game->asteroidCells();
|
||||
const bool challengeMode = game->getMode() == GameMode::Challenge;
|
||||
float impactStrength = 0.0f;
|
||||
float impactEased = 0.0f;
|
||||
std::array<uint8_t, Game::COLS * Game::ROWS> impactMask{};
|
||||
@ -954,7 +956,39 @@ void GameRenderer::renderPlayingState(
|
||||
bx += amplitude * std::sin(t * freq);
|
||||
by += amplitude * 0.75f * std::cos(t * (freq + 1.1f));
|
||||
}
|
||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
||||
|
||||
bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value();
|
||||
if (isAsteroid) {
|
||||
const AsteroidCell& cell = *asteroidCells[cellIdx];
|
||||
SDL_Color base{};
|
||||
switch (cell.type) {
|
||||
case AsteroidType::Normal: base = SDL_Color{172, 138, 104, 255}; break;
|
||||
case AsteroidType::Armored: base = SDL_Color{130, 150, 176, 255}; break;
|
||||
case AsteroidType::Falling: base = SDL_Color{210, 120, 82, 255}; break;
|
||||
case AsteroidType::Core: base = SDL_Color{198, 78, 200, 255}; break;
|
||||
}
|
||||
float hpScale = std::clamp(static_cast<float>(cell.hitsRemaining) / 3.0f, 0.25f, 1.0f);
|
||||
SDL_Color fill{
|
||||
static_cast<Uint8>(base.r * hpScale + 40 * (1.0f - hpScale)),
|
||||
static_cast<Uint8>(base.g * hpScale + 40 * (1.0f - hpScale)),
|
||||
static_cast<Uint8>(base.b * hpScale + 40 * (1.0f - hpScale)),
|
||||
255
|
||||
};
|
||||
drawRect(renderer, bx, by, finalBlockSize - 1, finalBlockSize - 1, fill);
|
||||
// Subtle outline to differentiate types
|
||||
SDL_Color outline = base;
|
||||
outline.a = 220;
|
||||
SDL_FRect border{bx + 1.0f, by + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f};
|
||||
SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a);
|
||||
SDL_RenderRect(renderer, &border);
|
||||
if (cell.gravityEnabled) {
|
||||
SDL_SetRenderDrawColor(renderer, 255, 230, 120, 180);
|
||||
SDL_FRect glow{bx + 2.0f, by + 2.0f, finalBlockSize - 4.0f, finalBlockSize - 4.0f};
|
||||
SDL_RenderRect(renderer, &glow);
|
||||
}
|
||||
} else {
|
||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1287,8 +1321,12 @@ void GameRenderer::renderPlayingState(
|
||||
|
||||
char levelStr[16];
|
||||
snprintf(levelStr, sizeof(levelStr), "%02d", game->level());
|
||||
char challengeLevelStr[16];
|
||||
snprintf(challengeLevelStr, sizeof(challengeLevelStr), "%02d/100", game->challengeLevel());
|
||||
char asteroidStr[32];
|
||||
snprintf(asteroidStr, sizeof(asteroidStr), "%d LEFT", game->asteroidsRemaining());
|
||||
|
||||
// Next level progress
|
||||
// Next level progress (endless only)
|
||||
int startLv = game->startLevelBase();
|
||||
int firstThreshold = (startLv + 1) * 10;
|
||||
int linesDone = game->lines();
|
||||
@ -1343,12 +1381,22 @@ void GameRenderer::renderPlayingState(
|
||||
statLines.push_back({scoreStr, 25.0f, 0.9f, valueColor});
|
||||
statLines.push_back({"LINES", 70.0f, 1.0f, labelColor});
|
||||
statLines.push_back({linesStr, 95.0f, 0.9f, valueColor});
|
||||
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
||||
statLines.push_back({levelStr, 165.0f, 0.9f, valueColor});
|
||||
statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor});
|
||||
statLines.push_back({nextStr, 225.0f, 0.9f, nextColor});
|
||||
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
||||
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
||||
|
||||
if (game->getMode() == GameMode::Challenge) {
|
||||
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
||||
statLines.push_back({challengeLevelStr, 165.0f, 0.9f, valueColor});
|
||||
statLines.push_back({"ASTEROIDS", 200.0f, 1.0f, labelColor});
|
||||
statLines.push_back({asteroidStr, 225.0f, 0.9f, nextColor});
|
||||
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
||||
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
||||
} else {
|
||||
statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor});
|
||||
statLines.push_back({levelStr, 165.0f, 0.9f, valueColor});
|
||||
statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor});
|
||||
statLines.push_back({nextStr, 225.0f, 0.9f, nextColor});
|
||||
statLines.push_back({"TIME", 265.0f, 1.0f, labelColor});
|
||||
statLines.push_back({timeStr, 290.0f, 0.9f, valueColor});
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
SDL_Color debugLabelColor{150, 150, 150, 255};
|
||||
|
||||
Reference in New Issue
Block a user