implement simulation shell
This commit is contained in:
@@ -4,6 +4,7 @@ SET(SRCS)
|
|||||||
add_subdirectory(core)
|
add_subdirectory(core)
|
||||||
add_subdirectory(config)
|
add_subdirectory(config)
|
||||||
add_subdirectory(utility)
|
add_subdirectory(utility)
|
||||||
|
add_subdirectory(sim)
|
||||||
|
|
||||||
SET(HDRS
|
SET(HDRS
|
||||||
${HDRS}
|
${HDRS}
|
||||||
|
|||||||
19
src/lib/sim/CMakeLists.txt
Normal file
19
src/lib/sim/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
SET(HDRS
|
||||||
|
${HDRS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
SET(SRCS
|
||||||
|
${SRCS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
set(LIB_INCLUDE_PATH
|
||||||
|
${LIB_INCLUDE_PATH}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
38
src/lib/sim/Simulation.cpp
Normal file
38
src/lib/sim/Simulation.cpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||||
|
: m_config(config)
|
||||||
|
, m_rng(seed)
|
||||||
|
, m_currentTick(0)
|
||||||
|
, m_nextId(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulation::tick()
|
||||||
|
{
|
||||||
|
++m_currentTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FireEvent> Simulation::drainFireEvents()
|
||||||
|
{
|
||||||
|
std::vector<FireEvent> result;
|
||||||
|
result.swap(m_fireEvents);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BlueprintDropEvent> Simulation::drainBlueprintDropEvents()
|
||||||
|
{
|
||||||
|
std::vector<BlueprintDropEvent> result;
|
||||||
|
result.swap(m_blueprintDropEvents);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tick Simulation::currentTick() const
|
||||||
|
{
|
||||||
|
return m_currentTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityId Simulation::allocateId()
|
||||||
|
{
|
||||||
|
return m_nextId++;
|
||||||
|
}
|
||||||
41
src/lib/sim/Simulation.h
Normal file
41
src/lib/sim/Simulation.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "EntityId.h"
|
||||||
|
#include "FireEvent.h"
|
||||||
|
#include "BlueprintDropEvent.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class Simulation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Simulation(const GameConfig& config, unsigned int seed = 0);
|
||||||
|
|
||||||
|
// Advances the simulation by one tick. Tick order per architecture.md §Tick Order.
|
||||||
|
// Currently a stub; subsystems are plugged in across Steps 3-7.
|
||||||
|
void tick();
|
||||||
|
|
||||||
|
// Returns all fire events accumulated since the last drain, clearing the
|
||||||
|
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
|
||||||
|
std::vector<FireEvent> drainFireEvents();
|
||||||
|
|
||||||
|
// Returns all blueprint drop events since the last drain.
|
||||||
|
std::vector<BlueprintDropEvent> drainBlueprintDropEvents();
|
||||||
|
|
||||||
|
Tick currentTick() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
EntityId allocateId(); // Strictly increasing; never returns kInvalidEntityId.
|
||||||
|
|
||||||
|
const GameConfig& m_config;
|
||||||
|
std::mt19937 m_rng;
|
||||||
|
|
||||||
|
Tick m_currentTick;
|
||||||
|
EntityId m_nextId; // starts at 1; 0 is kInvalidEntityId.
|
||||||
|
|
||||||
|
std::vector<FireEvent> m_fireEvents;
|
||||||
|
std::vector<BlueprintDropEvent> m_blueprintDropEvents;
|
||||||
|
};
|
||||||
21
src/lib/sim/TickDriver.cpp
Normal file
21
src/lib/sim/TickDriver.cpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#include "TickDriver.h"
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
int TickDriver::advance(double elapsedWallMs, double gameSpeedMultiplier)
|
||||||
|
{
|
||||||
|
m_accumulatorMs += elapsedWallMs * gameSpeedMultiplier;
|
||||||
|
|
||||||
|
int ticks = 0;
|
||||||
|
while (m_accumulatorMs >= kTickDurationMs)
|
||||||
|
{
|
||||||
|
m_accumulatorMs -= kTickDurationMs;
|
||||||
|
++ticks;
|
||||||
|
}
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TickDriver::reset()
|
||||||
|
{
|
||||||
|
m_accumulatorMs = 0.0;
|
||||||
|
}
|
||||||
21
src/lib/sim/TickDriver.h
Normal file
21
src/lib/sim/TickDriver.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Accumulator-based fixed-timestep driver. Decouples wall-clock render rate
|
||||||
|
// from the 30 Hz simulation tick rate. See architecture.md §Render Loop.
|
||||||
|
class TickDriver
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TickDriver() = default;
|
||||||
|
|
||||||
|
// Adds elapsedWallMs * gameSpeedMultiplier to the accumulator and returns
|
||||||
|
// how many simulation ticks should be stepped this frame. The remainder
|
||||||
|
// stays in the accumulator for the next call.
|
||||||
|
// A multiplier of 0.0 freezes accumulation (pause).
|
||||||
|
// Valid multipliers per REQ-UI-SPEED: 0, 0.5, 1, 2, 4.
|
||||||
|
int advance(double elapsedWallMs, double gameSpeedMultiplier);
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
double m_accumulatorMs = 0.0;
|
||||||
|
};
|
||||||
@@ -5,4 +5,5 @@ add_files(
|
|||||||
test.cpp
|
test.cpp
|
||||||
FormulaTest.cpp
|
FormulaTest.cpp
|
||||||
ConfigLoaderTest.cpp
|
ConfigLoaderTest.cpp
|
||||||
|
SimulationTest.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
133
src/test/SimulationTest.cpp
Normal file
133
src/test/SimulationTest.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
#include "TickDriver.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Simulation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
const Simulation sim(config);
|
||||||
|
|
||||||
|
REQUIRE(sim.currentTick() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
Simulation sim(config);
|
||||||
|
|
||||||
|
sim.tick();
|
||||||
|
|
||||||
|
REQUIRE(sim.currentTick() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
Simulation sim(config);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
sim.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(sim.currentTick() == 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
Simulation sim(config);
|
||||||
|
|
||||||
|
REQUIRE(sim.drainFireEvents().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
Simulation sim(config);
|
||||||
|
|
||||||
|
// First drain: empty.
|
||||||
|
sim.drainFireEvents();
|
||||||
|
|
||||||
|
// Second drain must also be empty (not a double-return).
|
||||||
|
REQUIRE(sim.drainFireEvents().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Simulation::drainBlueprintDropEvents returns empty initially", "[simulation]")
|
||||||
|
{
|
||||||
|
const GameConfig config;
|
||||||
|
Simulation sim(config);
|
||||||
|
|
||||||
|
REQUIRE(sim.drainBlueprintDropEvents().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TickDriver
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: 0x speed never produces ticks", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
REQUIRE(driver.advance(100.0, 0.0) == 0);
|
||||||
|
REQUIRE(driver.advance(1000.0, 0.0) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: exactly one tick duration at 1x produces 1 tick", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
REQUIRE(driver.advance(kTickDurationMs, 1.0) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: two tick durations at 1x produces 2 ticks", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
REQUIRE(driver.advance(2.0 * kTickDurationMs, 1.0) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: 4x speed with quarter tick duration produces 1 tick", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
REQUIRE(driver.advance(kTickDurationMs / 4.0, 4.0) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: 0.5x speed with double tick duration produces 1 tick", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
REQUIRE(driver.advance(kTickDurationMs / 0.5, 0.5) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver: accumulator carries partial progress across frames", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
// 60% of a tick — not enough to fire yet.
|
||||||
|
REQUIRE(driver.advance(kTickDurationMs * 0.6, 1.0) == 0);
|
||||||
|
|
||||||
|
// Another 60% — cumulative 120%, so exactly 1 tick fires.
|
||||||
|
REQUIRE(driver.advance(kTickDurationMs * 0.6, 1.0) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("TickDriver::reset clears the accumulator", "[simulation]")
|
||||||
|
{
|
||||||
|
TickDriver driver;
|
||||||
|
|
||||||
|
// Advance to just below one tick.
|
||||||
|
driver.advance(kTickDurationMs * 0.9, 1.0);
|
||||||
|
|
||||||
|
driver.reset();
|
||||||
|
|
||||||
|
// Nothing in the accumulator: zero elapsed time should not fire.
|
||||||
|
REQUIRE(driver.advance(0.0, 1.0) == 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user