World updates
Update World API and implementation: rename size fields to sizeX_/sizeY_/sizeZ_, add helpers such as mineAllPositive, positiveAverage, topPositiveSum and a sort helper, and fix related surface/mine semantics for dynamic columns.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,965 @@
|
||||
# SDL2 GUI Extension Guide: Parallel Deep Miner
|
||||
|
||||
> **Scope:** optional SDL2/SDL2_ttf renderer for the parallel Deep Miner project. This guide assumes the core implementation from `PARALLEL_DEEP_MINER_GUIDE.md`.
|
||||
>
|
||||
> The GUI is intentionally separated from the terminal/threading guide so the base project remains buildable without SDL2.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Purpose](#1-purpose)
|
||||
2. [Design Goals](#2-design-goals)
|
||||
3. [Prerequisites](#3-prerequisites)
|
||||
4. [Renderer Architecture](#4-renderer-architecture)
|
||||
5. [Renderer Public Interface](#5-renderer-public-interface)
|
||||
6. [Implementation Notes](#6-implementation-notes)
|
||||
7. [Integrating Renderer with Game](#7-integrating-renderer-with-game)
|
||||
8. [GUI CMake Target](#8-gui-cmake-target)
|
||||
9. [GUI Entry Point](#9-gui-entry-point)
|
||||
10. [Common Pitfalls](#10-common-pitfalls)
|
||||
11. [Appendix A: Complete GUI Source Code](#appendix-a-complete-gui-source-code)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
The SDL2 renderer provides a live window beside the terminal output. It visualizes:
|
||||
|
||||
- the 5×5 grid
|
||||
- surface values and effects
|
||||
- column depth bars
|
||||
- all 5–10 robots, not just two robots
|
||||
- live HP and scores
|
||||
- recent game messages
|
||||
- optional step-by-step or auto-advance playback
|
||||
|
||||
This guide replaces the old two-robot GUI design that referenced `player_`, `computer_`, `autoMode_`, and `play(Robot&, bool)`. The parallel architecture uses `std::vector<std::unique_ptr<Robot>>`, `robotLoop(idx)`, and a complete-turn mutex.
|
||||
|
||||
---
|
||||
|
||||
## 2. Design Goals
|
||||
|
||||
1. **Optional dependency**: the terminal build must not require SDL2 headers or libraries.
|
||||
2. **Parallel-compatible**: rendering works with the robot vector and does not assume exactly two robots.
|
||||
3. **No ownership confusion**: `Game` owns the robots; `Renderer` only observes them.
|
||||
4. **Stable snapshots**: render calls happen while the game state is stable.
|
||||
5. **Simple build split**: `deep_miner` remains terminal-only; `deep_miner_gui` links SDL2.
|
||||
|
||||
---
|
||||
|
||||
## 3. Prerequisites
|
||||
|
||||
### Arch Linux
|
||||
|
||||
```bash
|
||||
sudo pacman -S sdl2 sdl2_ttf
|
||||
```
|
||||
|
||||
### Ubuntu / Debian
|
||||
|
||||
```bash
|
||||
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf install SDL2-devel SDL2_ttf-devel
|
||||
```
|
||||
|
||||
The code can probe common system font paths at runtime. Do not bundle font files unless your project explicitly permits it.
|
||||
|
||||
---
|
||||
|
||||
## 4. Renderer Architecture
|
||||
|
||||
Use the pimpl idiom so SDL2 headers stay out of normal project headers.
|
||||
|
||||
```text
|
||||
include/Renderer.h src/Renderer.cpp
|
||||
------------------ ----------------
|
||||
class Renderer { #include <SDL2/SDL.h>
|
||||
struct Impl; #include <SDL2/SDL_ttf.h>
|
||||
std::unique_ptr<Impl> impl_; struct Renderer::Impl { ... };
|
||||
};
|
||||
```
|
||||
|
||||
Only `src/Renderer.cpp` includes SDL2 headers. `Game.h` can forward-declare `Renderer`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Renderer Public Interface
|
||||
|
||||
```cpp
|
||||
// include/Renderer.h
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class World;
|
||||
class Robot;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer();
|
||||
~Renderer();
|
||||
|
||||
Renderer(const Renderer&) = delete;
|
||||
Renderer& operator=(const Renderer&) = delete;
|
||||
|
||||
bool init();
|
||||
|
||||
void render(const World& world,
|
||||
const std::vector<const Robot*>& robots,
|
||||
int round,
|
||||
const std::vector<std::string>& log);
|
||||
|
||||
bool waitForStep();
|
||||
void setAutoAdvanceMs(int ms);
|
||||
bool isOpen() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
```
|
||||
|
||||
### Why pass `std::vector<const Robot*>`?
|
||||
|
||||
`Game` owns robots as `std::unique_ptr<Robot>`. The renderer should not know or care about ownership. A vector of raw `const Robot*` is a lightweight read-only view.
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Notes
|
||||
|
||||
### Layout
|
||||
|
||||
A practical 900×680 window layout:
|
||||
|
||||
```text
|
||||
+-------------------------------+----------------------------+
|
||||
| grid title / round | scores and HP |
|
||||
| | |
|
||||
| 5×5 grid | legend |
|
||||
| each cell shows surface value | message log |
|
||||
| and robot markers | controls |
|
||||
+-------------------------------+----------------------------+
|
||||
```
|
||||
|
||||
Recommended constants:
|
||||
|
||||
```cpp
|
||||
static constexpr int WIN_W = 900;
|
||||
static constexpr int WIN_H = 680;
|
||||
static constexpr int HDR_H = 56;
|
||||
static constexpr int GRID_OFF_X = 32;
|
||||
static constexpr int GRID_OFF_Y = 82;
|
||||
static constexpr int CELL_W = 96;
|
||||
static constexpr int CELL_H = 86;
|
||||
static constexpr int PANEL_X = 536;
|
||||
static constexpr int PANEL_PAD = 10;
|
||||
```
|
||||
|
||||
### Drawing multiple robots in one cell
|
||||
|
||||
A single cell may contain several robots. Draw small indexed markers instead of assuming one `P` and one `C`.
|
||||
|
||||
```cpp
|
||||
std::vector<int> robotsInCell;
|
||||
for (int i = 0; i < static_cast<int>(robots.size()); ++i) {
|
||||
if (robots[i]->isAlive()
|
||||
&& robots[i]->getX() == x
|
||||
&& robots[i]->getY() == y) {
|
||||
robotsInCell.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (int k = 0; k < static_cast<int>(robotsInCell.size()); ++k) {
|
||||
const int robotIndex = robotsInCell[k];
|
||||
const int cx = cellX + 18 + (k % 3) * 22;
|
||||
const int cy = cellY + 20 + (k / 3) * 22;
|
||||
|
||||
fillCircle(cx, cy, 9, robotColor(robotIndex));
|
||||
drawText(std::to_string(robotIndex + 1), cx - 4, cy - 7, white, false);
|
||||
}
|
||||
```
|
||||
|
||||
### Controls
|
||||
|
||||
| Key / action | Effect |
|
||||
|---|---|
|
||||
| `SPACE` / `ENTER` | advance immediately |
|
||||
| mouse click | advance immediately |
|
||||
| `ESC` | close the GUI window |
|
||||
| window close button | close the GUI window |
|
||||
| auto-advance | continue after configured milliseconds |
|
||||
|
||||
### waitForStep()
|
||||
|
||||
```cpp
|
||||
bool Renderer::waitForStep() {
|
||||
const Uint32 start = SDL_GetTicks();
|
||||
|
||||
while (impl_->open) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
impl_->open = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
const auto key = event.key.keysym.sym;
|
||||
if (key == SDLK_ESCAPE) {
|
||||
impl_->open = false;
|
||||
return false;
|
||||
}
|
||||
if (key == SDLK_SPACE || key == SDLK_RETURN) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (impl_->autoAdvanceMs > 0
|
||||
&& static_cast<int>(SDL_GetTicks() - start) >= impl_->autoAdvanceMs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_Delay(8);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Integrating Renderer with Game
|
||||
|
||||
### Game.h additions
|
||||
|
||||
```cpp
|
||||
// include/Game.h
|
||||
class Renderer;
|
||||
|
||||
class Game {
|
||||
public:
|
||||
Game();
|
||||
void run();
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
void setRenderer(Renderer* renderer) { renderer_ = renderer; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
// existing members
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
Renderer* renderer_ = nullptr; // non-owning
|
||||
std::vector<std::string> msgLog_; // capped message buffer
|
||||
int currentRound_ = 0;
|
||||
|
||||
void pushMsg(const std::string& msg);
|
||||
void renderFrameLocked();
|
||||
#endif
|
||||
};
|
||||
```
|
||||
|
||||
The pointer is non-owning. `main_gui.cpp` creates the renderer and passes its address to `Game`.
|
||||
|
||||
### Game.cpp includes
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
#include "../include/Renderer.h"
|
||||
#endif
|
||||
```
|
||||
|
||||
### Message logging
|
||||
|
||||
```cpp
|
||||
void Game::log(const std::string& msg) {
|
||||
std::cout << msg << '\n';
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
if (renderer_) {
|
||||
msgLog_.push_back(msg);
|
||||
if (msgLog_.size() > 120) {
|
||||
msgLog_.erase(msgLog_.begin());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
If you prefer to keep `log()` terminal-only, use a separate `pushMsg()` helper. The important point is that the GUI log must be updated from the same serialized turn path.
|
||||
|
||||
### Build a read-only robot view
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
std::vector<const Robot*> Game::robotView() const {
|
||||
std::vector<const Robot*> view;
|
||||
view.reserve(robots_.size());
|
||||
|
||||
for (const auto& robot : robots_) {
|
||||
view.push_back(robot.get());
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
If you add this helper, declare it in `Game.h` under the same `#ifdef`.
|
||||
|
||||
### renderFrameLocked()
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
void Game::renderFrameLocked() {
|
||||
if (!renderer_ || !renderer_->isOpen()) return;
|
||||
|
||||
std::vector<const Robot*> view;
|
||||
view.reserve(robots_.size());
|
||||
for (const auto& robot : robots_) {
|
||||
view.push_back(robot.get());
|
||||
}
|
||||
|
||||
renderer_->render(world_, view, currentRound_, msgLog_);
|
||||
renderer_->waitForStep();
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
The name `renderFrameLocked()` is deliberate: call it only while `turnMutex_` is held. This guarantees the renderer sees a consistent snapshot. In GUI step mode, the game intentionally pauses while the user views the frame.
|
||||
|
||||
### Calling the renderer from robotLoop()
|
||||
|
||||
```cpp
|
||||
void Game::robotLoop(int idx) {
|
||||
ScopedTimer threadTimer(threadTimes_[idx]);
|
||||
Robot& robot = *robots_[idx];
|
||||
|
||||
while (true) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(turnMutex_);
|
||||
|
||||
if (!robot.isAlive()) {
|
||||
recordDeath(idx, "killed earlier");
|
||||
#ifdef WITH_SDL2_GUI
|
||||
renderFrameLocked();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
if (isGameOver()) {
|
||||
break;
|
||||
}
|
||||
|
||||
++currentRound_;
|
||||
|
||||
const int dir = robot.decideNextMove(world_);
|
||||
robot.move(dir, world_);
|
||||
|
||||
const int effect = world_.checkEffects(robot.getX(), robot.getY());
|
||||
if (effect < 0) applyEffect(robot, effect);
|
||||
|
||||
if (!robot.isAlive()) {
|
||||
recordDeath(idx, "killed by effect");
|
||||
#ifdef WITH_SDL2_GUI
|
||||
renderFrameLocked();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
fightNearby(robot);
|
||||
|
||||
if (effect != -1 && robot.isAlive()) {
|
||||
const int mined = robot.mine(world_);
|
||||
log(robot.getName() + " mined " + std::to_string(mined) + " points.");
|
||||
}
|
||||
|
||||
checkRearrange(robot);
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
renderFrameLocked();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This preserves the base thread model. The GUI mode slows the simulation because it pauses after turns, but it does not reintroduce `player_`, `computer_`, or a sequential `play()` loop.
|
||||
|
||||
### Initial and final frames
|
||||
|
||||
Render once after setup and once after all threads join. Both should happen while no robot thread is mutating the game state.
|
||||
|
||||
```cpp
|
||||
void Game::run() {
|
||||
setup();
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(turnMutex_);
|
||||
renderFrameLocked();
|
||||
}
|
||||
#endif
|
||||
|
||||
// launch and join threads
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
if (renderer_ && renderer_->isOpen()) {
|
||||
renderer_->setAutoAdvanceMs(0);
|
||||
std::lock_guard<std::mutex> lk(turnMutex_);
|
||||
log("=== GAME OVER ===");
|
||||
renderFrameLocked();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. GUI CMake Target
|
||||
|
||||
Keep the terminal target independent from SDL2.
|
||||
|
||||
```cmake
|
||||
option(BUILD_GUI "Build SDL2 GUI target" OFF)
|
||||
|
||||
if (BUILD_GUI)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_ttf REQUIRED)
|
||||
|
||||
add_executable(deep_miner_gui
|
||||
${SOURCES}
|
||||
src/Renderer.cpp
|
||||
main_gui.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(deep_miner_gui PRIVATE WITH_SDL2_GUI)
|
||||
target_include_directories(deep_miner_gui PRIVATE ${SDL2_INCLUDE_DIRS})
|
||||
target_link_libraries(deep_miner_gui
|
||||
Threads::Threads
|
||||
SDL2::SDL2
|
||||
SDL2_ttf::SDL2_ttf
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
Depending on your platform's CMake package files, the imported targets may be named differently. If `SDL2::SDL2` or `SDL2_ttf::SDL2_ttf` is unavailable, use the variables exported by your SDL2 CMake package.
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DBUILD_GUI=ON ..
|
||||
cmake --build .
|
||||
./deep_miner_gui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. GUI Entry Point
|
||||
|
||||
```cpp
|
||||
// main_gui.cpp
|
||||
#include "include/Game.h"
|
||||
#include "include/Renderer.h"
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
Renderer renderer;
|
||||
if (!renderer.init()) {
|
||||
std::cerr << "Could not initialise SDL2 renderer.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
renderer.setAutoAdvanceMs(900);
|
||||
|
||||
Game game;
|
||||
game.setRenderer(&renderer);
|
||||
game.run();
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Fatal error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The normal terminal `main.cpp` remains unchanged and does not include `Renderer.h`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Common Pitfalls
|
||||
|
||||
### Pitfall 1: Including SDL2 in Game.h
|
||||
|
||||
Do not include `<SDL2/SDL.h>` in `Game.h`. Forward-declare `Renderer` and include SDL2 only in `Renderer.cpp`.
|
||||
|
||||
### Pitfall 2: Rendering two hard-coded robots
|
||||
|
||||
Do not use `player_`, `computer_`, `P`, or `C` as architectural concepts. The game has 5–10 robots. Render indexed markers or abbreviated robot names.
|
||||
|
||||
### Pitfall 3: Waiting for input without a stable snapshot
|
||||
|
||||
If the renderer waits while other threads mutate the world, the displayed state can become inconsistent. The simple solution is to call `renderFrameLocked()` while `turnMutex_` is held. For a more advanced solution, copy a lightweight snapshot under the lock and let the renderer wait after the lock is released.
|
||||
|
||||
### Pitfall 4: Linking SDL2 into the terminal target
|
||||
|
||||
Only `deep_miner_gui` should link SDL2. The base `deep_miner` target should link only `Threads::Threads`.
|
||||
|
||||
### Pitfall 5: GUI timing changes thread measurements
|
||||
|
||||
Step mode and auto-advance delays are part of the measured runtime if you call the renderer inside `robotLoop()`. This is acceptable for visualization. For benchmark runs, use the terminal target.
|
||||
|
||||
---
|
||||
|
||||
## Minimal Project Additions
|
||||
|
||||
```text
|
||||
deep_miner/
|
||||
|-- main_gui.cpp
|
||||
|-- include/
|
||||
| `-- Renderer.h
|
||||
`-- src/
|
||||
`-- Renderer.cpp
|
||||
```
|
||||
|
||||
The GUI guide deliberately stays separate from the base programming guide. The core project remains a clean terminal-based C++17 threading assignment, while this extension adds visualization without changing the ownership model or robot architecture.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Complete GUI Source Code
|
||||
|
||||
This appendix contains the complete GUI-specific files plus the small `Game` integration points required by the renderer. The core project files remain in `PARALLEL_DEEP_MINER_GUIDE.md`.
|
||||
|
||||
### `main_gui.cpp`
|
||||
|
||||
```cpp
|
||||
#include "Game.h"
|
||||
#include "Renderer.h"
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
Renderer renderer;
|
||||
if (!renderer.init()) {
|
||||
std::cerr << "Could not initialise SDL2 renderer.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
renderer.setAutoAdvanceMs(900);
|
||||
Game game;
|
||||
game.setRenderer(&renderer);
|
||||
game.run();
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Fatal error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `include/Renderer.h`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class World;
|
||||
class Robot;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer();
|
||||
~Renderer();
|
||||
|
||||
bool init();
|
||||
void render(const World& world,
|
||||
const std::vector<Robot*>& robots,
|
||||
int round,
|
||||
const std::vector<std::string>& log);
|
||||
|
||||
bool waitForStep();
|
||||
void setAutoAdvanceMs(int ms);
|
||||
bool isOpen() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
```
|
||||
|
||||
### `src/Renderer.cpp`
|
||||
|
||||
```cpp
|
||||
#include "Renderer.h"
|
||||
#include "Robot.h"
|
||||
#include "World.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
SDL_Color rgba(Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) { return SDL_Color{r, g, b, a}; }
|
||||
}
|
||||
|
||||
struct Renderer::Impl {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Renderer* sdl = nullptr;
|
||||
TTF_Font* fontLg = nullptr;
|
||||
TTF_Font* fontSm = nullptr;
|
||||
bool open = false;
|
||||
int autoAdvanceMs = 900;
|
||||
|
||||
static constexpr int WIN_W = 920;
|
||||
static constexpr int WIN_H = 700;
|
||||
static constexpr int GRID_X = 32;
|
||||
static constexpr int GRID_Y = 84;
|
||||
static constexpr int CELL_W = 92;
|
||||
static constexpr int CELL_H = 82;
|
||||
static constexpr int PANEL_X = 535;
|
||||
|
||||
bool init() {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) return false;
|
||||
if (TTF_Init() != 0) return false;
|
||||
|
||||
window = SDL_CreateWindow("Parallel Deep Miner",
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
WIN_W, WIN_H,
|
||||
SDL_WINDOW_SHOWN);
|
||||
if (!window) return false;
|
||||
|
||||
sdl = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
|
||||
if (!sdl) return false;
|
||||
|
||||
const char* candidates[] = {
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
"C:/Windows/Fonts/arial.ttf"
|
||||
};
|
||||
for (const char* path : candidates) {
|
||||
if (!fontLg) fontLg = TTF_OpenFont(path, 20);
|
||||
if (!fontSm) fontSm = TTF_OpenFont(path, 13);
|
||||
if (fontLg && fontSm) break;
|
||||
}
|
||||
|
||||
open = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (fontLg) TTF_CloseFont(fontLg);
|
||||
if (fontSm) TTF_CloseFont(fontSm);
|
||||
if (sdl) SDL_DestroyRenderer(sdl);
|
||||
if (window) SDL_DestroyWindow(window);
|
||||
fontLg = nullptr;
|
||||
fontSm = nullptr;
|
||||
sdl = nullptr;
|
||||
window = nullptr;
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void setColor(SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(sdl, c.r, c.g, c.b, c.a);
|
||||
SDL_SetRenderDrawBlendMode(sdl, c.a < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
void fillRect(int x, int y, int w, int h, SDL_Color c) {
|
||||
setColor(c);
|
||||
SDL_Rect r{x, y, w, h};
|
||||
SDL_RenderFillRect(sdl, &r);
|
||||
}
|
||||
|
||||
void strokeRect(int x, int y, int w, int h, SDL_Color c) {
|
||||
setColor(c);
|
||||
SDL_Rect r{x, y, w, h};
|
||||
SDL_RenderDrawRect(sdl, &r);
|
||||
}
|
||||
|
||||
void fillCircle(int cx, int cy, int radius, SDL_Color c) {
|
||||
setColor(c);
|
||||
for (int dy = -radius; dy <= radius; ++dy) {
|
||||
int dx = static_cast<int>(std::sqrt(radius * radius - dy * dy));
|
||||
SDL_RenderDrawLine(sdl, cx - dx, cy + dy, cx + dx, cy + dy);
|
||||
}
|
||||
}
|
||||
|
||||
void drawText(const std::string& text, int x, int y, SDL_Color color, bool large = false) {
|
||||
TTF_Font* font = large ? fontLg : fontSm;
|
||||
if (!font || text.empty()) return;
|
||||
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, text.c_str(), color);
|
||||
if (!surface) return;
|
||||
SDL_Texture* texture = SDL_CreateTextureFromSurface(sdl, surface);
|
||||
SDL_Rect dst{x, y, surface->w, surface->h};
|
||||
SDL_FreeSurface(surface);
|
||||
if (!texture) return;
|
||||
SDL_RenderCopy(sdl, texture, nullptr, &dst);
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
|
||||
SDL_Color cellColor(int value) const {
|
||||
if (value == 0) return rgba(28, 31, 35);
|
||||
if (value == -1) return rgba(176, 96, 28);
|
||||
if (value == -2) return rgba(88, 55, 140);
|
||||
if (value == -3) return rgba(170, 40, 55);
|
||||
int clamped = std::clamp(value, 1, 9);
|
||||
Uint8 g = static_cast<Uint8>(70 + clamped * 15);
|
||||
Uint8 r = static_cast<Uint8>(20 + clamped * 22);
|
||||
return rgba(r, g, 55);
|
||||
}
|
||||
|
||||
void drawGrid(const World& world, const std::vector<Robot*>& robots) {
|
||||
for (int y = 0; y < world.getSizeY(); ++y) {
|
||||
for (int x = 0; x < world.getSizeX(); ++x) {
|
||||
int px = GRID_X + x * CELL_W;
|
||||
int py = GRID_Y + y * CELL_H;
|
||||
int value = world.getSurfaceValue(x, y);
|
||||
fillRect(px, py, CELL_W - 8, CELL_H - 8, cellColor(value));
|
||||
strokeRect(px, py, CELL_W - 8, CELL_H - 8, rgba(230, 230, 230, 90));
|
||||
|
||||
std::string label = value == 0 ? "--" : std::to_string(value);
|
||||
drawText(label, px + 32, py + 24, rgba(245, 245, 245), true);
|
||||
|
||||
int depth = world.getSurfaceLevel(x, y) + 1;
|
||||
int barW = static_cast<int>((CELL_W - 12) * (static_cast<double>(depth) / world.getSizeZ()));
|
||||
fillRect(px, py + CELL_H - 15, barW, 5, rgba(240, 240, 240, 150));
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < robots.size(); ++i) {
|
||||
Robot* r = robots[i];
|
||||
if (!r || !r->isAlive()) continue;
|
||||
int px = GRID_X + r->getX() * CELL_W + 18 + static_cast<int>((i % 3) * 18);
|
||||
int py = GRID_Y + r->getY() * CELL_H + 14 + static_cast<int>((i / 3) * 14);
|
||||
fillCircle(px, py, 10, rgba(80, 190, 240));
|
||||
drawText(std::to_string(i + 1), px - 4, py - 8, rgba(10, 10, 10));
|
||||
}
|
||||
}
|
||||
|
||||
void drawPanel(const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) {
|
||||
fillRect(PANEL_X, 0, WIN_W - PANEL_X, WIN_H, rgba(20, 22, 26));
|
||||
drawText("Round " + std::to_string(round), PANEL_X + 18, 24, rgba(245, 245, 245), true);
|
||||
drawText("Robots", PANEL_X + 18, 70, rgba(230, 230, 230), true);
|
||||
|
||||
int y = 105;
|
||||
for (std::size_t i = 0; i < robots.size(); ++i) {
|
||||
Robot* r = robots[i];
|
||||
if (!r) continue;
|
||||
std::ostringstream oss;
|
||||
oss << (i + 1) << ". " << r->getName() << " score=" << r->getScore() << " hp=" << r->getHp();
|
||||
drawText(oss.str(), PANEL_X + 18, y, r->isAlive() ? rgba(235, 235, 235) : rgba(160, 160, 160));
|
||||
y += 22;
|
||||
}
|
||||
|
||||
y += 18;
|
||||
drawText("Messages", PANEL_X + 18, y, rgba(230, 230, 230), true);
|
||||
y += 34;
|
||||
int start = std::max(0, static_cast<int>(log.size()) - 15);
|
||||
for (int i = start; i < static_cast<int>(log.size()); ++i) {
|
||||
drawText(log[i], PANEL_X + 18, y, rgba(210, 210, 210));
|
||||
y += 20;
|
||||
}
|
||||
}
|
||||
|
||||
void render(const World& world, const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) {
|
||||
if (!open) return;
|
||||
setColor(rgba(12, 14, 18));
|
||||
SDL_RenderClear(sdl);
|
||||
drawText("DEEP MINER", 32, 24, rgba(245, 245, 245), true);
|
||||
drawGrid(world, robots);
|
||||
drawPanel(robots, round, log);
|
||||
drawText("SPACE/click: step ESC/window close: quit", 32, WIN_H - 36, rgba(210, 210, 210));
|
||||
SDL_RenderPresent(sdl);
|
||||
}
|
||||
|
||||
bool waitForStep() {
|
||||
if (!open) return false;
|
||||
Uint32 start = SDL_GetTicks();
|
||||
SDL_Event event;
|
||||
while (open) {
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) { open = false; return false; }
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
if (event.key.keysym.sym == SDLK_ESCAPE) { open = false; return false; }
|
||||
if (event.key.keysym.sym == SDLK_SPACE || event.key.keysym.sym == SDLK_RETURN) return true;
|
||||
}
|
||||
if (event.type == SDL_MOUSEBUTTONDOWN) return true;
|
||||
}
|
||||
if (autoAdvanceMs > 0 && SDL_GetTicks() - start >= static_cast<Uint32>(autoAdvanceMs)) return true;
|
||||
SDL_Delay(10);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Renderer::Renderer() : impl_(std::make_unique<Impl>()) {}
|
||||
Renderer::~Renderer() { impl_->shutdown(); }
|
||||
bool Renderer::init() { return impl_->init(); }
|
||||
void Renderer::render(const World& world, const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) { impl_->render(world, robots, round, log); }
|
||||
bool Renderer::waitForStep() { return impl_->waitForStep(); }
|
||||
void Renderer::setAutoAdvanceMs(int ms) { impl_->autoAdvanceMs = ms; }
|
||||
bool Renderer::isOpen() const { return impl_->open; }
|
||||
```
|
||||
|
||||
### `CMakeLists.txt` GUI additions
|
||||
|
||||
```cmake
|
||||
option(BUILD_GUI "Build SDL2 GUI target" OFF)
|
||||
|
||||
if (BUILD_GUI)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_ttf REQUIRED)
|
||||
|
||||
add_executable(deep_miner_gui
|
||||
${SOURCES}
|
||||
src/Renderer.cpp
|
||||
main_gui.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(deep_miner_gui PRIVATE WITH_SDL2_GUI)
|
||||
target_include_directories(deep_miner_gui PRIVATE ${SDL2_INCLUDE_DIRS})
|
||||
target_link_libraries(deep_miner_gui
|
||||
Threads::Threads
|
||||
SDL2::SDL2
|
||||
SDL2_ttf::SDL2_ttf
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
### `include/Game.h` GUI integration additions
|
||||
|
||||
Add a forward declaration before the `Game` class:
|
||||
|
||||
```cpp
|
||||
class Renderer;
|
||||
```
|
||||
|
||||
Add this public method:
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
void setRenderer(Renderer* renderer);
|
||||
#endif
|
||||
```
|
||||
|
||||
Add these private members and helper declarations:
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
Renderer* renderer_ = nullptr;
|
||||
std::vector<std::string> guiLog_;
|
||||
void renderFrameLocked();
|
||||
std::vector<Robot*> robotPointers() const;
|
||||
#endif
|
||||
```
|
||||
|
||||
### `src/Game.cpp` GUI integration additions
|
||||
|
||||
Include the renderer only for the GUI build:
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
#include "Renderer.h"
|
||||
#endif
|
||||
```
|
||||
|
||||
Add these methods:
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
void Game::setRenderer(Renderer* renderer) {
|
||||
renderer_ = renderer;
|
||||
}
|
||||
|
||||
std::vector<Robot*> Game::robotPointers() const {
|
||||
std::vector<Robot*> out;
|
||||
out.reserve(robots_.size());
|
||||
for (const auto& robot : robots_) out.push_back(robot.get());
|
||||
return out;
|
||||
}
|
||||
|
||||
void Game::renderFrameLocked() {
|
||||
if (!renderer_ || !renderer_->isOpen()) return;
|
||||
renderer_->render(world_, robotPointers(), round_, guiLog_);
|
||||
renderer_->waitForStep();
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
Modify `Game::log` so GUI messages are recorded while terminal output remains unchanged:
|
||||
|
||||
```cpp
|
||||
void Game::log(const std::string& message) const {
|
||||
std::cout << message << "\n";
|
||||
#ifdef WITH_SDL2_GUI
|
||||
auto* self = const_cast<Game*>(this);
|
||||
self->guiLog_.push_back(message);
|
||||
if (self->guiLog_.size() > 120) self->guiLog_.erase(self->guiLog_.begin());
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
Call `renderFrameLocked()` after each atomic turn, before releasing the mutex:
|
||||
|
||||
```cpp
|
||||
// near the end of Game::robotLoop(), still inside the lock_guard scope
|
||||
checkRearrange(robot);
|
||||
#ifdef WITH_SDL2_GUI
|
||||
renderFrameLocked();
|
||||
#endif
|
||||
```
|
||||
|
||||
Render an initial and final frame in `Game::run()`:
|
||||
|
||||
```cpp
|
||||
#ifdef WITH_SDL2_GUI
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(turnMutex_);
|
||||
renderFrameLocked();
|
||||
}
|
||||
#endif
|
||||
|
||||
// launch and join threads here
|
||||
|
||||
#ifdef WITH_SDL2_GUI
|
||||
if (renderer_ && renderer_->isOpen()) {
|
||||
renderer_->setAutoAdvanceMs(0);
|
||||
std::lock_guard<std::mutex> lock(turnMutex_);
|
||||
guiLog_.push_back("=== GAME OVER ===");
|
||||
renderFrameLocked();
|
||||
}
|
||||
#endif
|
||||
```
|
||||
Reference in New Issue
Block a user