Crafter.Debug/examples/Basic/implementations/main.cpp
Jorijn van der Graaf 4a0d911e69
feat(examples): Basic, a ball simulation with a bug to find
The example was a placeholder that printed which build it was. It is
now a headless simulation of five balls dropped onto a 10 cm floor
slab at 60 ticks a second, with a bug planted the way such bugs are
written: collision is checked only where each step lands, so the ball
dropped from 60 m, at 34 m/s, steps over the slab and falls forever.

It shows the consumer side end to end:
- Basic.DebugShim forwards Pump, Advance, QuitRequested, Log, Check,
  Emit and Sample to Crafter.Debug in debug builds and does nothing in
  plain ones, so the simulation and main have no #ifdef but the
  session's.
- Basic.DebugCommands, compiled only when Debug::Use returns true,
  registers World_Spawn, World_Reset, World_Clear and Ball_Get, and
  watches for the physics, the floor and the balls.
- The simulation checks each ball stays above the floor, emits Bounce
  and samples the fastest ball, so `pause --on-check`, `wait --event`
  and `trace --history` have something to show.

Driven over the real transport, it shows the whole loop: `log --level
warn` names the ball, `pause --on-check` stops the frame it fails,
`set physics.substeps 4` and `call World_Reset` show the fix holds
before any code changes. The plain build simulates ten seconds and
prints that one ball fell through. Its agent notes describe the app
without the bug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:22:17 +01:00

58 lines
2 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// A headless simulation at 60 ticks a second. The plain build simulates ten
// seconds as fast as it can and prints what happened; the debug build runs in
// real time, serving Crafter.Debug, until an agent says quit.
import std;
import Basic.DebugShim;
import Basic.World;
#ifdef BASIC_DEBUG_API
import Crafter.Thread;
import Basic.DebugCommands;
#endif
namespace Debug = Crafter::Debug;
using Clock = std::chrono::steady_clock;
namespace {
constexpr float dt = 1.0f / 60.0f;
#ifdef BASIC_DEBUG_API
constexpr bool realTime = true;
#else
constexpr bool realTime = false;
#endif
}
int main() {
Basic::World world;
world.Reset();
#ifdef BASIC_DEBUG_API
// The session's transport runs on Crafter.Thread's pool. An app that
// already uses the pool starts it as usual; this one only needs it here.
Crafter::ThreadPool::Start();
Debug::Session debug({ .name = "basic" });
Basic::RegisterDebug(world);
std::println("basic: running; drive it with ./build/crafter-debug, stop it with ./build/crafter-debug quit");
#endif
const auto tick = std::chrono::duration_cast<Clock::duration>(std::chrono::duration<float>(dt));
Clock::time_point next = Clock::now();
while (!Debug::QuitRequested() && (realTime || world.Time() < 10.0)) {
Debug::Pump();
if (Debug::Advance()) world.Tick(dt);
if constexpr (realTime) {
next += tick;
// After a stop in a debugger, carry on from now instead of
// racing to catch up.
if (Clock::now() - next > std::chrono::seconds(1)) next = Clock::now();
std::this_thread::sleep_until(next);
}
}
std::println("basic: {:.1f} s simulated; {} balls, {} below the floor", world.Time(), world.Balls().size(), world.Lost());
for (const Basic::Ball& ball : world.Balls()) {
if (ball.lost) std::println(" ball {} fell through the floor", ball.id);
}
}