Files
dusk/test/rpg/battle/test_battle.c
T
YourWishesandClaude Sonnet 5 3675ec634f Rework battle system: pointer-based fighters, per-state files, move split
Splits battlemoveaction_t into its own header, adds a per-state
struct/init/update/dispose file under rpg/battle/state driven by a
dispatch table, switches BATTLE.fighters to battlefighter_t* backed by a
pool, folds current/max health and mp into battlefighterstats_t, and adds
battleFighterDamage/Heal/AbilityAdd. Removes the now-unused battle
cutscene items (START_BATTLE, BATTLE_WAIT_STATE, BATTLE_FORCE_MOVE,
REGULAR_BATTLE) and their JSON lookup helpers, and adds JSON validation
to battleMoveInit. battle.h/battle.c are mid-rewrite around the new state
machine, so some UI consumers (uibattlehud.c, uibattlemenu.c) and
test_battle.c are currently out of sync with the new API.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-19 23:17:27 -05:00

272 lines
9.0 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "rpg/battle/battle.h"
// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup.
#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1)
static battlefighter_t *addFighter(
const battlefighterteam_t team,
const battlefightercontroller_t controller,
const uint16_t attack,
const uint16_t defense,
const uint16_t speed,
const uint16_t healthMax
) {
const battlefighterstats_t stats = {
.attack = attack, .defense = defense, .speed = speed
};
return battleAddFighter(team, controller, stats, healthMax, 0);
}
static void test_battleStartEntersOpeningThenPreRound(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 10, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 10, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
assert_int_equal(BATTLE.state, BATTLE_STATE_OPENING);
// OPENING only transitions to PRE_ROUND; it doesn't run PRE_ROUND's logic
// in the same call.
battleUpdate();
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
assert_int_equal(BATTLE.executionCount, 0);
// PRE_ROUND builds the execution order and moves on to selection.
battleUpdate();
assert_int_equal(BATTLE.executionCount, 2);
assert_true(
BATTLE.state == BATTLE_STATE_PLAYER_SELECTION ||
BATTLE.state == BATTLE_STATE_AI_SELECTION
);
}
static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters(
void **state
) {
battleInit();
battlefighter_t *ally1 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 100, 20
);
battlefighter_t *ally2 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 50, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION, positions the cursor
// ally1 is fastest, so it's first to decide.
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
assert_ptr_equal(battleGetCurrentFighter(), ally1);
// A forced move (as a scripted-battle cutscene item would push) marks
// ally2 as already decided, so PLAYER_SELECTION must skip it.
battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id);
battlePlayerAttack(enemy->id);
assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK);
assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id);
// ally2 already had a decision queued, and enemy is AI-controlled, so
// there's nothing left for PLAYER_SELECTION -- it should have moved on.
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
}
static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// ally decides, selection moves to AI
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
battleUpdate();// AI_SELECTION auto-decides for enemy
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK);
assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id);
}
static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles(
void **state
) {
battleInit();
// ally1 is fast enough to kill enemy1 before enemy1 or ally2 act. enemy2
// keeps the battle alive so execution continues to ally2's now-moot
// attack against the already-dead enemy1.
battlefighter_t *ally1 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 100, 20
);
battlefighter_t *ally2 = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
);
battlefighter_t *enemy1 = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 50, 10
);
battlefighter_t *enemy2 = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 1, 100
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION (ally1 first, fastest)
assert_ptr_equal(battleGetCurrentFighter(), ally1);
battlePlayerAttack(enemy1->id);
assert_ptr_equal(battleGetCurrentFighter(), ally2);
battlePlayerAttack(enemy1->id);// both allies target enemy1
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
battleUpdate();// AI_SELECTION
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
// Execution order: ally1 (100), enemy1 (50), ally2 (10), enemy2 (1).
battleUpdate();// ally1 kills enemy1
assert_false(battleFighterIsAlive(enemy1));
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);// enemy2 alive
battleUpdate();// enemy1's own (now dead) turn is skipped
battleUpdate();// ally2's attack against the dead enemy1 fizzles silently
assert_int_equal(enemy1->health, 0);// no double-kill, no underflow
battleUpdate();// enemy2 acts
battleUpdate();// all 4 slots processed -> POST_ROUND
assert_int_equal(BATTLE.state, BATTLE_STATE_POST_ROUND);
battleUpdate();// POST_ROUND -> PRE_ROUND, round advances
assert_int_equal(BATTLE.round, 2);
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
}
static void test_battleWinEndsInStateEnded(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 10
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
battleUpdate();// ally kills the only enemy
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_WIN);
}
static void test_battleLossEndsInStateEnded(void **state) {
battleInit();
battlefighter_t *ally = addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 5, 10
);
battlefighter_t *enemy = addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 50, 0, 10, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
battleUpdate();// ally attacks first (irrelevant to outcome)
battleUpdate();// enemy kills the only ally
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_LOSS);
}
static void test_battleFleeEndsInStateEndedImmediately(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerFlee();
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
assert_int_equal(BATTLE.result, BATTLE_RESULT_FLED);
}
static void test_battleFleeUnavailableIsIgnored(void **state) {
battleInit();
addFighter(
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
);
addFighter(
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
);
battleStart(BATTLE_ENCOUNTER_REGULAR, false);
battleUpdate();// OPENING -> PRE_ROUND
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
battlePlayerFlee();
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE);
}
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_battleStartEntersOpeningThenPreRound),
cmocka_unit_test(test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters),
cmocka_unit_test(test_battleAiSelectionAutoQueuesUndecidedAiFighters),
cmocka_unit_test(test_battleMovesExecutingResolvesInSpeedOrderAndFizzles),
cmocka_unit_test(test_battleWinEndsInStateEnded),
cmocka_unit_test(test_battleLossEndsInStateEnded),
cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately),
cmocka_unit_test(test_battleFleeUnavailableIsIgnored),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}