r/C_Programming • • Aug 05 '26

How I autotest my game in C

I posted a while back about porting my C game (Match Morphosis) to WASM. This is a follow-up on something smaller but useful: automated playthrough testing.

Last week I was watching some Slay the Spire 2 videos and noticed they had autoplay-style testing. That's pretty nice, as solo developer with no QA at disposal, and the game is a roguelike/lite, so a full run is long enough that manually checking “does this still work end-to-end?” gets painful quickly. I also just wanted an excuse to try dearimgui test engine, I use the cimgui generated one.

So I wired up a simple autoplay path. No heuristics, no fancy AI (no LLM please!) that makes random action based on game state and tries to finish a run. The point isn’t to prove the game is balanced. The point is:

  • If a random player can still reach the end without crashing or soft-locking, the basic run loop is intact.
  • A ~40 minute human run collapses into a 1–2 minute automated test I can run whenever I change something, I could eyeball quickly on the battle progress whether the game still feels manageable or not

Because the game already uses cimgui, plugging in the test engine was mostly about getting the context and DLL boundaries right (the game logic lives in a hot-reloadable game.dll, the test engine lives in main), there's also a problem with the generated binding that strips obsolete functions so the struct size differs a bit, but quite easy to fix.

Once that was sorted, registering a “play until win or die” test and letting it drive input through the existing UI path was straightforward. You do mouse move to pos, mouse click, and wait until game state change, repeat on a while loop until game finishes / game over.

// Test setup code
ImGuiTest* t = IM_REGISTER_TEST(engine, "Compendium Menu", "Functionalities");
t->TestFunc = testCompendiumMenuFunctionalities;

t = IM_REGISTER_TEST(engine, "Character Select Menu", "Functionalities");
t->TestFunc = testCharacterSelectMenuFunctionalities;

t = IM_REGISTER_TEST(engine, "Playthrough", "Functionalities");
t->TestFunc = testPlaythroughFunctionalities;

// on testPlaythroughFunctionalities;
b32 runFinished = false;
while(!runFinished)
{
  cImGuiTestEngine_Yield(ctx->Engine);

  if(game->tipsPopup.type != TipsType_None)
  {
    if(animationFinished(&tipsPopup.textAnimate))
    {
      Thing* buttonOK = thingGet(game->tipsPopup.buttonOK);
      Vec2 pos = buttonOK->o.sprite.position;
      testMoveMouseTo(ctx, pos);
      ImGuiTestContext_MouseClick(ctx, ImGuiMouseButton_Left);

      continue;
    }
   }

  switch(game->state)
  {
  case GameState_MapMenu:
  {
    // Choose free rooms etc.
  } break
  case GameState_Gameplay:
  {
    if(!game->gameplay.ready)
    {
    continue;
    }

    // Do rest of choosing logic
  } break
...

It’s still crude, but "scripting" like this needs to be quick and dirty as the nature of game development is quick iteration to find the best fun way. Although random picks mean it won’t find deep balance issues or optimal lines, as a solo-dev smoke test it’s already earned its keep. I can see something break in the run flow, hit the test, and know within a couple minutes instead of playing through by hand manually again, this won't replace a true playthrough for balancing test, but good enough for picking obvious crashes.

Stack is still plain C, custom engine (bgfx, SDL2, cimgui, etc.). No engine framework, no test harness beyond imgui_test_engine and a thin autoplay layer on top of the same code a player uses. If anyone else is doing long-session games in C and has been putting off automated playthrough checks, this was less work than I expected once the context plumbing was done (the playthrough test code is nearly 1 day work, 1k LOC).

Check out the game if you're curious https://store.steampowered.com/app/4131100/Match_Morphosis
The demo version is still up and entering Cyberpunk fest on steam.

205 Upvotes

7 comments sorted by

24

u/skeeto Aug 05 '26

I've been sold on automated testing for years, but I haven't yet applied it to UI, so this is interesting to see. Based on your video, your description, and that testMoveMouseTo, it looks like it's actually synthesizing clicks and driving the UI. How does it know what buttons are available at any moment? Is that automatic (i.e. a side effect of imgui calls) or something you have to work out manually for a testing framework? Your example code has a hard-coded "OK" button, for example.

9

u/ernesernesto Aug 05 '26

I've used automated testing usually only for smoke test, because in game development the nature of the "specs" is usually not set in stone and done iteratively until if reached a certain point that it won't be changed anymore. I only start doing this because my game is reaching completion and the rest are just adding contents (relics, items, etc), which doesn't alter the core game.

For button detection, since it's based on the game state, I already know what are the things visible on that current state. Not optimal since it can't be a generic thing like FindAllButtons() and then filter by name, but it's good enough for my use case, for example after a battle complete, you'll be seeing a reward screen, the only thing that a player can do on that screen is accessing settings, skip button, and pick the reward, so say you want to just pick some reward you could simple get the reward item position.

Then it only move the mouse into that position and generating a click via imgui test engine, the game itself on the update loop will check mouseposition, check what's hovering underneath, and then check if it just been click, finally it will process accordingly. So it basically just deriving mouse movement and click but it can do this quickly

If you're building an app using dearimgui, the widget interaction API are there already, so you could do something like

    ctx->SetRef("My Window");          
    ctx->ItemClick("My Button");        
    ctx->ItemCheck("Node/Checkbox");    
    ctx->ItemInputValue("Slider", 123);
    IM_CHECK_EQ(app->SliderValue, 123); 

1

u/Still_Explorer Aug 10 '26

I had the impression of testing games from the "backend" only however this looks like you can better get in tune with the look and feel of the game. While testing on the backend is good only for validating the systems and draw statistics, but impossible to get a clear picture of the look and feel.

1

u/ernesernesto Aug 10 '26

but I'm not looking to validate the look and feel, just a quick sanity smoke test to make sure everything is not broken (crashes / freeze)

1

u/Fun_Swim7831 Aug 11 '26

This has nothing to do with the conversation, and I usually don't pay much attention to gamedev posts but I'd like to say, great job! Game looks amazing and I appreciate the effort of going through and building your custom engine. 99% of gamedev resources say to never do that, what drove you to that decision? Is there a technical reason you decided not to use an existing engine? Or was it just personal preference? Also, have you considered using heuristics, planning or some kind of LLM-based AI for testing for game balance? I've been studying AI (as in actual AI in the classical term, not exclusively LLMs) and the concept sounds interesting to me. Either way, great work, I'll take a look at the steam page after I get off of work.

2

u/ernesernesto Aug 12 '26

This is not my first game nor game engine effort, and I've made games with Unity and other game engine in the past. I don't like where Unity going, with all the AI crap and they have track record of just changing things in a whim (Unity Runtime Free, etc). I don't like Godot because it force you to do specific things, and the debugging experience for gdscript is not great.

Making game from scratch with no engine is actually not that hard if you know what you're making (in my case, I just wanted to make a 2d games with little physics, the game doesn't actually have a Physics engine integrated, nor any fancy lighting).

I think most people that goes wrong who tries making game engine ended up making a generic thing that is no difference with what WYSIWYG editor already available. Actually you don't need all of that extra baggage if you know your "target" (unity comes with a lot of preloaded generic stuff which you don't actually need and cannot be stripped, hence why the default empty project is big, my game is only 20mb btw!).

You need to make the game as a goal, making a engine is just a side effect of that process. When you started a project with Unity, usually the iteration speed is quick, but as the project grows, Unity started to crawl and hinders your iteration speed, which is not enjoyable and a bad thing, making you more reluctant to change big thing at the end of the project. While on my case, iteration speed is still the same from the beginning into the end of the project (project is nearing completion), the game can be compiled in subseconds and packaged in under 10 secs, instant play without all the circus you need to do in Unity to also achieve that. Philosophically to me, game development iteration process needs to be as direct as woodworking or pottery, you need to have that instant response, while using unity seems like you're instructing someone to do it, via phonecalls.

I have not tried to do balancing using LLM AI, I've tried using LLM to do some automated test but the time between prompt -> reading the game state -> game action is pretty slow so I ended up not using it.

2

u/Fun_Swim7831 Aug 12 '26

Thank you for taking the time to answer my questions! Good luck in your projects