implement belt system
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
|
belt_speed_tiles_per_second = 2
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width = 40
|
asteroid_width = 40
|
||||||
|
|||||||
@@ -219,9 +219,10 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
|
|
||||||
WorldConfig cfg;
|
WorldConfig cfg;
|
||||||
|
|
||||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||||
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
||||||
|
cfg.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second");
|
||||||
|
|
||||||
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
|
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
|
||||||
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));
|
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ struct WorldWaves
|
|||||||
|
|
||||||
struct WorldConfig
|
struct WorldConfig
|
||||||
{
|
{
|
||||||
int heightTiles; // REQ-GW-HEIGHT
|
int heightTiles; // REQ-GW-HEIGHT
|
||||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
int refundPercentage; // REQ-BLD-DEMOLISH
|
||||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||||
|
double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED
|
||||||
|
|
||||||
WorldRegions regions;
|
WorldRegions regions;
|
||||||
WorldExpansion expansion;
|
WorldExpansion expansion;
|
||||||
|
|||||||
373
src/lib/sim/BeltSystem.cpp
Normal file
373
src/lib/sim/BeltSystem.cpp
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
#include "BeltSystem.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::pair<int, int> BeltSystem::key(QPoint tile)
|
||||||
|
{
|
||||||
|
return {tile.x(), tile.y()};
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint BeltSystem::adjacentTile(QPoint tile, Rotation dir)
|
||||||
|
{
|
||||||
|
switch (dir)
|
||||||
|
{
|
||||||
|
case Rotation::North: return {tile.x(), tile.y() - 1};
|
||||||
|
case Rotation::East: return {tile.x() + 1, tile.y() };
|
||||||
|
case Rotation::South: return {tile.x(), tile.y() + 1};
|
||||||
|
case Rotation::West: return {tile.x() - 1, tile.y() };
|
||||||
|
}
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress)
|
||||||
|
{
|
||||||
|
// Map progress [0, 1] along the belt direction to a fractional tile-unit position.
|
||||||
|
// Progress 0 = entered from opposite side; 1 = at output edge.
|
||||||
|
double baseX = tile.x() + 0.5;
|
||||||
|
double baseY = tile.y() + 0.5;
|
||||||
|
|
||||||
|
switch (dir)
|
||||||
|
{
|
||||||
|
case Rotation::North: return {baseX, baseY - (progress - 0.5)};
|
||||||
|
case Rotation::East: return {baseX + (progress - 0.5), baseY};
|
||||||
|
case Rotation::South: return {baseX, baseY + (progress - 0.5)};
|
||||||
|
case Rotation::West: return {baseX - (progress - 0.5), baseY};
|
||||||
|
}
|
||||||
|
return {baseX, baseY};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Construction / placement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BeltSystem::BeltSystem(double beltSpeedTilesPerSecond)
|
||||||
|
: m_progressPerTick(beltSpeedTilesPerSecond * kTickDurationSeconds)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::placeBelt(QPoint tile, Rotation direction)
|
||||||
|
{
|
||||||
|
m_splitters.erase(key(tile));
|
||||||
|
BeltTile bt;
|
||||||
|
bt.direction = direction;
|
||||||
|
m_belts[key(tile)] = bt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
|
||||||
|
{
|
||||||
|
m_belts.erase(key(tile));
|
||||||
|
SplitterTile st;
|
||||||
|
st.outputA = outputA;
|
||||||
|
st.outputB = outputB;
|
||||||
|
st.nextOutputIsA = true;
|
||||||
|
m_splitters[key(tile)] = st;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::removeTile(QPoint tile)
|
||||||
|
{
|
||||||
|
m_belts.erase(key(tile));
|
||||||
|
m_splitters.erase(key(tile));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::setSplitterFilters(QPoint tile,
|
||||||
|
const std::vector<ItemType>& filterA,
|
||||||
|
const std::vector<ItemType>& filterB)
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.find(key(tile));
|
||||||
|
if (it == m_splitters.end())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it->second.filterA = filterA;
|
||||||
|
it->second.filterB = filterB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Port interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool BeltSystem::tryPutItem(Port port, Item item)
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile));
|
||||||
|
if (it == m_belts.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (it->second.direction != port.direction)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return tryPlaceOnBelt(port.tile, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile));
|
||||||
|
if (it == m_belts.end())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (it->second.direction != port.direction)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeltTile& bt = it->second;
|
||||||
|
if (bt.front)
|
||||||
|
{
|
||||||
|
const Item taken = bt.front->item;
|
||||||
|
bt.front = bt.back;
|
||||||
|
bt.back = std::nullopt;
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
if (bt.back)
|
||||||
|
{
|
||||||
|
const Item taken = bt.back->item;
|
||||||
|
bt.back = std::nullopt;
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Maintenance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
||||||
|
{
|
||||||
|
for (const QPoint& tile : tiles)
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
|
||||||
|
if (bIt != m_belts.end())
|
||||||
|
{
|
||||||
|
bIt->second.front = std::nullopt;
|
||||||
|
bIt->second.back = std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
|
||||||
|
if (sIt != m_splitters.end())
|
||||||
|
{
|
||||||
|
sIt->second.heldItem = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tick
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void BeltSystem::tick()
|
||||||
|
{
|
||||||
|
advanceProgress();
|
||||||
|
moveItemsToNextTile();
|
||||||
|
routeSplitterItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::advanceProgress()
|
||||||
|
{
|
||||||
|
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
|
||||||
|
it != m_belts.end(); ++it)
|
||||||
|
{
|
||||||
|
BeltTile& bt = it->second;
|
||||||
|
|
||||||
|
if (bt.front)
|
||||||
|
{
|
||||||
|
bt.front->progress += m_progressPerTick;
|
||||||
|
if (bt.front->progress > 1.0)
|
||||||
|
{
|
||||||
|
bt.front->progress = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bt.back)
|
||||||
|
{
|
||||||
|
bt.back->progress += m_progressPerTick;
|
||||||
|
|
||||||
|
// Back must not overtake front.
|
||||||
|
if (bt.front && bt.back->progress >= bt.front->progress)
|
||||||
|
{
|
||||||
|
bt.back->progress = bt.front->progress - m_progressPerTick;
|
||||||
|
if (bt.back->progress < 0.0)
|
||||||
|
{
|
||||||
|
bt.back->progress = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bt.back->progress > 1.0)
|
||||||
|
{
|
||||||
|
bt.back->progress = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::moveItemsToNextTile()
|
||||||
|
{
|
||||||
|
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
|
||||||
|
it != m_belts.end(); ++it)
|
||||||
|
{
|
||||||
|
BeltTile& bt = it->second;
|
||||||
|
if (!bt.front || bt.front->progress < 1.0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint here = QPoint(it->first.first, it->first.second);
|
||||||
|
const QPoint next = adjacentTile(here, bt.direction);
|
||||||
|
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::iterator nextBelt = m_belts.find(key(next));
|
||||||
|
const std::map<std::pair<int, int>, SplitterTile>::iterator nextSplitter = m_splitters.find(key(next));
|
||||||
|
|
||||||
|
if (nextBelt != m_belts.end())
|
||||||
|
{
|
||||||
|
if (tryPlaceOnBelt(next, bt.front->item))
|
||||||
|
{
|
||||||
|
bt.front = bt.back;
|
||||||
|
bt.back = std::nullopt;
|
||||||
|
}
|
||||||
|
// else: next belt is full — item stays blocked at progress 1.0.
|
||||||
|
}
|
||||||
|
else if (nextSplitter != m_splitters.end())
|
||||||
|
{
|
||||||
|
if (!nextSplitter->second.heldItem)
|
||||||
|
{
|
||||||
|
nextSplitter->second.heldItem = bt.front->item;
|
||||||
|
bt.front = bt.back;
|
||||||
|
bt.back = std::nullopt;
|
||||||
|
}
|
||||||
|
// else: splitter busy — item stays blocked at progress 1.0.
|
||||||
|
}
|
||||||
|
// else: no tile registered (e.g. open space, or building input port).
|
||||||
|
// Items leaving into unregistered tiles are not consumed here — the
|
||||||
|
// building pull step uses tryTakeItem for that.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltSystem::routeSplitterItems()
|
||||||
|
{
|
||||||
|
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
|
||||||
|
it != m_splitters.end(); ++it)
|
||||||
|
{
|
||||||
|
SplitterTile& st = it->second;
|
||||||
|
if (!st.heldItem)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item& item = *st.heldItem;
|
||||||
|
|
||||||
|
const bool matchesA = st.filterA.empty() ||
|
||||||
|
std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
|
||||||
|
const bool matchesB = st.filterB.empty() ||
|
||||||
|
std::find(st.filterB.begin(), st.filterB.end(), item.type) != st.filterB.end();
|
||||||
|
|
||||||
|
if (matchesA && !matchesB)
|
||||||
|
{
|
||||||
|
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputA);
|
||||||
|
if (tryPlaceOnBelt(dest, item))
|
||||||
|
{
|
||||||
|
st.heldItem = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (matchesB && !matchesA)
|
||||||
|
{
|
||||||
|
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputB);
|
||||||
|
if (tryPlaceOnBelt(dest, item))
|
||||||
|
{
|
||||||
|
st.heldItem = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (matchesA && matchesB)
|
||||||
|
{
|
||||||
|
// Alternation: try preferred output first, fall back to other.
|
||||||
|
const Rotation preferred = st.nextOutputIsA ? st.outputA : st.outputB;
|
||||||
|
const Rotation fallback = st.nextOutputIsA ? st.outputB : st.outputA;
|
||||||
|
|
||||||
|
const QPoint prefDest = adjacentTile(QPoint(it->first.first, it->first.second), preferred);
|
||||||
|
const QPoint fbDest = adjacentTile(QPoint(it->first.first, it->first.second), fallback);
|
||||||
|
|
||||||
|
if (tryPlaceOnBelt(prefDest, item))
|
||||||
|
{
|
||||||
|
st.heldItem = std::nullopt;
|
||||||
|
st.nextOutputIsA = !st.nextOutputIsA;
|
||||||
|
}
|
||||||
|
else if (tryPlaceOnBelt(fbDest, item))
|
||||||
|
{
|
||||||
|
st.heldItem = std::nullopt;
|
||||||
|
// nextOutputIsA stays: preferred was blocked, so we still owe it next.
|
||||||
|
}
|
||||||
|
// else both blocked — item stays.
|
||||||
|
}
|
||||||
|
// else (!matchesA && !matchesB): stall — item stays in splitter.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(tile));
|
||||||
|
if (it == m_belts.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeltTile& bt = it->second;
|
||||||
|
|
||||||
|
if (!bt.front)
|
||||||
|
{
|
||||||
|
bt.front = BeltItemSlot{item, 0.0};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!bt.back)
|
||||||
|
{
|
||||||
|
bt.back = BeltItemSlot{item, 0.0};
|
||||||
|
|
||||||
|
// Ensure ordering invariant: front has higher progress.
|
||||||
|
if (bt.back->progress > bt.front->progress)
|
||||||
|
{
|
||||||
|
std::swap(bt.front, bt.back);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // both slots occupied
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rendering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||||
|
std::function<void(VisualItem)> visit) const
|
||||||
|
{
|
||||||
|
for (const std::pair<const std::pair<int, int>, BeltTile>& entry : m_belts)
|
||||||
|
{
|
||||||
|
const QPoint tile(entry.first.first, entry.first.second);
|
||||||
|
if (!viewportTiles.contains(tile))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeltTile& bt = entry.second;
|
||||||
|
|
||||||
|
if (bt.front)
|
||||||
|
{
|
||||||
|
VisualItem vi;
|
||||||
|
vi.type = bt.front->item.type;
|
||||||
|
vi.worldPos = slotWorldPos(tile, bt.direction, bt.front->progress);
|
||||||
|
visit(vi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bt.back)
|
||||||
|
{
|
||||||
|
VisualItem vi;
|
||||||
|
vi.type = bt.back->item.type;
|
||||||
|
vi.worldPos = slotWorldPos(tile, bt.direction, bt.back->progress);
|
||||||
|
visit(vi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/lib/sim/BeltSystem.h
Normal file
117
src/lib/sim/BeltSystem.h
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "Item.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Port.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Carries item type and fractional world position for the renderer.
|
||||||
|
// worldPos is in tile units (1 tile = 1.0 unit); origin matches tile coords.
|
||||||
|
struct VisualItem
|
||||||
|
{
|
||||||
|
ItemType type;
|
||||||
|
QPointF worldPos;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Isolated belt-and-splitter transport layer. See architecture.md §Belt Subsystem.
|
||||||
|
//
|
||||||
|
// Buildings interact only through tryPutItem / tryTakeItem.
|
||||||
|
// Rendering reads only through forEachVisualItem.
|
||||||
|
// No other system inspects tile contents.
|
||||||
|
class BeltSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit BeltSystem(double beltSpeedTilesPerSecond);
|
||||||
|
|
||||||
|
// -- Placement -----------------------------------------------------------
|
||||||
|
// Register a new belt tile. Any items already on this tile are cleared.
|
||||||
|
void placeBelt(QPoint tile, Rotation direction);
|
||||||
|
|
||||||
|
// Register a new splitter tile. outputA and outputB are the two exit
|
||||||
|
// directions (e.g. West and East for a default-rotation splitter).
|
||||||
|
// Items entering from any adjacent belt whose direction points into this
|
||||||
|
// tile are held and routed to one of the two outputs.
|
||||||
|
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
|
||||||
|
|
||||||
|
// Remove a belt or splitter tile (on demolish). Items are discarded.
|
||||||
|
void removeTile(QPoint tile);
|
||||||
|
|
||||||
|
// -- Splitter filter configuration (REQ-BLD-SPLITTER) -------------------
|
||||||
|
// filterA / filterB: empty means "accept all".
|
||||||
|
void setSplitterFilters(QPoint tile,
|
||||||
|
const std::vector<ItemType>& filterA,
|
||||||
|
const std::vector<ItemType>& filterB);
|
||||||
|
|
||||||
|
// -- Port interface (buildings <-> belts) --------------------------------
|
||||||
|
// port.tile = the belt tile adjacent to the building
|
||||||
|
// port.direction = direction items flow on that tile
|
||||||
|
//
|
||||||
|
// tryPutItem: place item onto port.tile entering from the opposite side.
|
||||||
|
// Returns false if the tile is not a belt, direction mismatches, or tile full.
|
||||||
|
bool tryPutItem(Port port, Item item);
|
||||||
|
|
||||||
|
// tryTakeItem: remove and return the leading item from port.tile.
|
||||||
|
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
|
||||||
|
std::optional<Item> tryTakeItem(Port port);
|
||||||
|
|
||||||
|
// -- Maintenance ---------------------------------------------------------
|
||||||
|
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||||
|
void tick();
|
||||||
|
|
||||||
|
// -- Rendering -----------------------------------------------------------
|
||||||
|
void forEachVisualItem(QRect viewportTiles,
|
||||||
|
std::function<void(VisualItem)> visit) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void advanceProgress();
|
||||||
|
void moveItemsToNextTile();
|
||||||
|
void routeSplitterItems();
|
||||||
|
|
||||||
|
// Place item into back slot of an existing belt tile at progress 0.
|
||||||
|
// Returns false if tile is not a belt or is full.
|
||||||
|
bool tryPlaceOnBelt(QPoint tile, Item item);
|
||||||
|
|
||||||
|
static std::pair<int, int> key(QPoint tile);
|
||||||
|
static QPoint adjacentTile(QPoint tile, Rotation dir);
|
||||||
|
|
||||||
|
// Returns the world-space centre of a slot given tile origin and progress.
|
||||||
|
static QPointF slotWorldPos(QPoint tile, Rotation dir, double progress);
|
||||||
|
|
||||||
|
struct BeltItemSlot
|
||||||
|
{
|
||||||
|
Item item;
|
||||||
|
double progress; // [0.0, 1.0]: 0 = just entered, 1 = at output edge
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BeltTile
|
||||||
|
{
|
||||||
|
Rotation direction;
|
||||||
|
std::optional<BeltItemSlot> front; // higher progress; closer to output
|
||||||
|
std::optional<BeltItemSlot> back; // lower progress; closer to input
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SplitterTile
|
||||||
|
{
|
||||||
|
Rotation outputA;
|
||||||
|
Rotation outputB;
|
||||||
|
std::vector<ItemType> filterA; // empty = accept all
|
||||||
|
std::vector<ItemType> filterB;
|
||||||
|
bool nextOutputIsA; // alternation state
|
||||||
|
std::optional<Item> heldItem; // item buffered waiting to exit
|
||||||
|
};
|
||||||
|
|
||||||
|
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
||||||
|
|
||||||
|
std::map<std::pair<int, int>, BeltTile> m_belts;
|
||||||
|
std::map<std::pair<int, int>, SplitterTile> m_splitters;
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ SET(HDRS
|
|||||||
${HDRS}
|
${HDRS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ SET(SRCS
|
|||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
356
src/test/BeltSystemTest.cpp
Normal file
356
src/test/BeltSystemTest.cpp
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "Item.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Port.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// Belt speed of 30 t/s means progress/tick = 30/30 = 1.0.
|
||||||
|
// One tick() call advances any item exactly one full tile.
|
||||||
|
// This makes test assertions on item positions simple and deterministic.
|
||||||
|
static constexpr double kFastBeltSpeed = static_cast<double>(kTickRateHz);
|
||||||
|
|
||||||
|
static Item makeItem(const std::string& id)
|
||||||
|
{
|
||||||
|
Item item;
|
||||||
|
item.type.id = id;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Port eastPort(QPoint tile)
|
||||||
|
{
|
||||||
|
Port p;
|
||||||
|
p.tile = tile;
|
||||||
|
p.direction = Rotation::East;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Placement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryPutItem succeeds on registered belt with matching direction", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
|
||||||
|
REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryPutItem fails on unregistered tile", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryPutItem(eastPort(QPoint(0, 0)), makeItem("iron_ore")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryPutItem fails on direction mismatch", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::North);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.removeTile(tile);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Capacity
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: two items fit in one tile", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
|
||||||
|
REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore")));
|
||||||
|
REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("copper_ore")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: third tryPutItem on full tile returns false", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("a"));
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("b"));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("c")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tryTakeItem
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryTakeItem returns placed item", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("iron_ore"));
|
||||||
|
|
||||||
|
const std::optional<Item> taken = bs.tryTakeItem(eastPort(tile));
|
||||||
|
|
||||||
|
REQUIRE(taken.has_value());
|
||||||
|
REQUIRE(taken->type.id == "iron_ore");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryTakeItem with two items returns both in sequence", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("first"));
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("second"));
|
||||||
|
|
||||||
|
// First take returns front item (first placed, higher progress).
|
||||||
|
const std::optional<Item> taken1 = bs.tryTakeItem(eastPort(tile));
|
||||||
|
REQUIRE(taken1.has_value());
|
||||||
|
|
||||||
|
// Second take returns the remaining item.
|
||||||
|
const std::optional<Item> taken2 = bs.tryTakeItem(eastPort(tile));
|
||||||
|
REQUIRE(taken2.has_value());
|
||||||
|
|
||||||
|
// Tile is now empty.
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryTakeItem returns nullopt on empty tile", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
bs.placeBelt(QPoint(0, 0), Rotation::East);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(QPoint(0, 0))).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: tryTakeItem returns nullopt on direction mismatch", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::North);
|
||||||
|
bs.tryPutItem(Port{tile, Rotation::North}, makeItem("x"));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tick() — item advancement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: one tick moves item from tile A to tile B in a 2-tile chain", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tileA(0, 0);
|
||||||
|
const QPoint tileB(1, 0);
|
||||||
|
bs.placeBelt(tileA, Rotation::East);
|
||||||
|
bs.placeBelt(tileB, Rotation::East);
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileA), makeItem("iron_ore"));
|
||||||
|
bs.tick();
|
||||||
|
|
||||||
|
// Item should have moved to tileB.
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
||||||
|
const std::optional<Item> inB = bs.tryTakeItem(eastPort(tileB));
|
||||||
|
REQUIRE(inB.has_value());
|
||||||
|
REQUIRE(inB->type.id == "iron_ore");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: item stays at progress 1.0 when next tile is absent", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tileA(0, 0);
|
||||||
|
bs.placeBelt(tileA, Rotation::East);
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileA), makeItem("iron_ore"));
|
||||||
|
bs.tick();
|
||||||
|
|
||||||
|
// Item should still be on tileA (no registered tile to the east).
|
||||||
|
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: item traverses 3-tile chain in 2 ticks", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tileA(0, 0);
|
||||||
|
const QPoint tileB(1, 0);
|
||||||
|
const QPoint tileC(2, 0);
|
||||||
|
bs.placeBelt(tileA, Rotation::East);
|
||||||
|
bs.placeBelt(tileB, Rotation::East);
|
||||||
|
bs.placeBelt(tileC, Rotation::East);
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileA), makeItem("iron_ore"));
|
||||||
|
bs.tick(); // A -> B
|
||||||
|
bs.tick(); // B -> C
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileB)).has_value());
|
||||||
|
REQUIRE(bs.tryTakeItem(eastPort(tileC)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tileA(0, 0);
|
||||||
|
const QPoint tileB(1, 0);
|
||||||
|
bs.placeBelt(tileA, Rotation::East);
|
||||||
|
bs.placeBelt(tileB, Rotation::East);
|
||||||
|
|
||||||
|
// Fill tileB to capacity.
|
||||||
|
bs.tryPutItem(eastPort(tileB), makeItem("b1"));
|
||||||
|
bs.tryPutItem(eastPort(tileB), makeItem("b2"));
|
||||||
|
|
||||||
|
// Place item in tileA — should be blocked.
|
||||||
|
bs.tryPutItem(eastPort(tileA), makeItem("a1"));
|
||||||
|
bs.tick();
|
||||||
|
|
||||||
|
// Item in tileA must still be there.
|
||||||
|
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// clearTiles
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: clearTiles removes all items from specified tiles", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("iron_ore"));
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("copper_ore"));
|
||||||
|
|
||||||
|
bs.clearTiles({tile});
|
||||||
|
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// forEachVisualItem
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: forEachVisualItem visits items inside viewport", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(5, 5);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("iron_ore"));
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; });
|
||||||
|
|
||||||
|
REQUIRE(count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: forEachVisualItem skips items outside viewport", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(50, 50);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("iron_ore"));
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; });
|
||||||
|
|
||||||
|
REQUIRE(count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
bs.tryPutItem(eastPort(tile), makeItem("copper_ingot"));
|
||||||
|
|
||||||
|
std::vector<ItemType> seen;
|
||||||
|
bs.forEachVisualItem(QRect(-1, -1, 10, 10), [&seen](VisualItem vi)
|
||||||
|
{
|
||||||
|
seen.push_back(vi.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
REQUIRE(seen.size() == 1);
|
||||||
|
REQUIRE(seen[0].id == "copper_ingot");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Splitter — basic alternation (no filters)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]")
|
||||||
|
{
|
||||||
|
// Layout: tileIn -> splitter -> tileA (West output)
|
||||||
|
// -> tileB (East output)
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
const QPoint tileA(1, -1); // North of splitter
|
||||||
|
const QPoint tileB(1, 1); // South of splitter
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
bs.placeBelt(tileA, Rotation::North);
|
||||||
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileIn), makeItem("item1"));
|
||||||
|
bs.tick(); // item moves: tileIn -> splitter held
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileIn), makeItem("item2"));
|
||||||
|
bs.tick(); // item1 routes to outputA (North=tileA); item2 moves to splitter
|
||||||
|
|
||||||
|
bs.tick(); // item2 routes to outputB (South=tileB)
|
||||||
|
|
||||||
|
const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value();
|
||||||
|
const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value();
|
||||||
|
|
||||||
|
// One item in each output — alternation worked.
|
||||||
|
REQUIRE(inA);
|
||||||
|
REQUIRE(inB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Splitter — filter routing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
const QPoint tileA(1, -1); // North output
|
||||||
|
const QPoint tileB(1, 1); // South output
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
bs.placeBelt(tileA, Rotation::North);
|
||||||
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
|
// Filter: outputA = iron_ore only; outputB = accept all.
|
||||||
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {});
|
||||||
|
|
||||||
|
bs.tryPutItem(eastPort(tileIn), makeItem("iron_ore"));
|
||||||
|
bs.tick(); // tileIn -> splitter held
|
||||||
|
bs.tick(); // routed to outputA (filter match)
|
||||||
|
|
||||||
|
REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value());
|
||||||
|
REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value());
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ add_files(
|
|||||||
FormulaTest.cpp
|
FormulaTest.cpp
|
||||||
ConfigLoaderTest.cpp
|
ConfigLoaderTest.cpp
|
||||||
SimulationTest.cpp
|
SimulationTest.cpp
|
||||||
|
BeltSystemTest.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
|||||||
// world.toml
|
// world.toml
|
||||||
REQUIRE(cfg.world.heightTiles == 60);
|
REQUIRE(cfg.world.heightTiles == 60);
|
||||||
REQUIRE(cfg.world.refundPercentage == 75);
|
REQUIRE(cfg.world.refundPercentage == 75);
|
||||||
|
REQUIRE(cfg.world.beltSpeedTilesPerSecond == Approx(2.0));
|
||||||
REQUIRE(cfg.world.regions.asteroidWidth == 40);
|
REQUIRE(cfg.world.regions.asteroidWidth == 40);
|
||||||
REQUIRE(cfg.world.regions.playerBufferWidth == 10);
|
REQUIRE(cfg.world.regions.playerBufferWidth == 10);
|
||||||
REQUIRE(cfg.world.regions.enemyBufferWidth == 15);
|
REQUIRE(cfg.world.regions.enemyBufferWidth == 15);
|
||||||
@@ -161,6 +162,7 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi
|
|||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
|
belt_speed_tiles_per_second = 2
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width = 40
|
asteroid_width = 40
|
||||||
@@ -204,6 +206,7 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification
|
|||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
|
belt_speed_tiles_per_second = 2
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width = 40
|
asteroid_width = 40
|
||||||
@@ -220,7 +223,7 @@ push_expand_columns = 20
|
|||||||
scaling_factor = 1.2
|
scaling_factor = 1.2
|
||||||
|
|
||||||
[waves]
|
[waves]
|
||||||
threat_rate_formula = "1 * + x"
|
threat_rate_formula = "1 +"
|
||||||
ship_level_formula = "1 + x / 120"
|
ship_level_formula = "1 + x / 120"
|
||||||
gap_min_seconds = 15
|
gap_min_seconds = 15
|
||||||
gap_max_seconds = 45
|
gap_max_seconds = 45
|
||||||
@@ -248,6 +251,7 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]")
|
|||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
|
belt_speed_tiles_per_second = 2
|
||||||
|
|
||||||
[regions]
|
[regions]
|
||||||
asteroid_width = 40
|
asteroid_width = 40
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ TEST_CASE("Formula retains its source string", "[formula]")
|
|||||||
|
|
||||||
TEST_CASE("Formula throws on malformed source", "[formula]")
|
TEST_CASE("Formula throws on malformed source", "[formula]")
|
||||||
{
|
{
|
||||||
REQUIRE_THROWS_AS(Formula::compile("1 * + x"), std::runtime_error);
|
REQUIRE_THROWS_AS(Formula::compile("1 +"), std::runtime_error);
|
||||||
REQUIRE_THROWS_AS(Formula::compile(""), std::runtime_error);
|
REQUIRE_THROWS_AS(Formula::compile(""), std::runtime_error);
|
||||||
REQUIRE_THROWS_AS(Formula::compile("unknown_var"), std::runtime_error);
|
REQUIRE_THROWS_AS(Formula::compile("unknown_var"), std::runtime_error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user