1
0
forked from 0ad/0ad

Add some tests for UnitAI.

Fix said tests for UnitAI.
Hopefully fix #647 too.
Document HFSM interface a bit.
Add Engine.DumpSimState() console command for debugging.

This was SVN commit r8681.
This commit is contained in:
Ykkrosh 2010-11-22 20:12:04 +00:00
parent 78ae72d87a
commit f378a63d94
6 changed files with 257 additions and 10 deletions

View File

@ -235,9 +235,6 @@ var UnitFsmSpec = {
},
"IDLE": {
"enter": function() {
this.SelectAnimation("idle");
},
},
"WALKING": {
@ -261,6 +258,11 @@ var UnitFsmSpec = {
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
// No orders left, we're an individual now
this.SetNextState("INDIVIDUAL.IDLE");
},
@ -294,6 +296,10 @@ var UnitFsmSpec = {
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
@ -302,11 +308,10 @@ var UnitFsmSpec = {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents))
return true;
return true; // (abort the transition since we may have already switched state)
}
// Nobody to attack - switch to idle
this.SelectAnimation("idle");
// Nobody to attack - stay in idle
return false;
},
@ -728,8 +733,9 @@ UnitAI.prototype.SetupRangeQuery = function(owner)
var players = [];
if(owner != -1)
{ // If unit not just killed, get enemy players via diplomacy
if (owner != -1)
{
// If unit not just killed, get enemy players via diplomacy
var player = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
// Get our diplomacy array
@ -737,7 +743,8 @@ UnitAI.prototype.SetupRangeQuery = function(owner)
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{ // Exclude gaia, allies, and self
{
// Exclude gaia, allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (i != owner && diplomacy[i - 1] < 0)
players.push(i);

View File

@ -28,11 +28,34 @@ Engine.QueryInterface = function(ent, iid)
return null;
};
Engine.RegisterGlobal = function(name, value)
{
global[name] = value;
};
Engine.DestroyEntity = function(ent)
{
for (var cid in g_Components[ent])
{
var cmp = g_Components[ent][cid];
if (cmp.Deinit)
cmp.Deinit();
}
delete g_Components[ent];
// TODO: should send Destroy message
};
// TODO:
// Engine.RegisterGlobal
// Engine.PostMessage
// Engine.BroadcastMessage
global.ResetState = function()
{
g_Components = {};
};
global.AddMock = function(ent, iid, mock)
{
if (!g_Components[ent])
@ -46,5 +69,10 @@ global.ConstructComponent = function(ent, name, template)
cmp.entity = ent;
cmp.template = template;
cmp.Init();
if (!g_Components[ent])
g_Components[ent] = {};
g_Components[ent][g_ComponentTypes[name].iid] = cmp;
return cmp;
};

View File

@ -0,0 +1,130 @@
Engine.LoadHelperScript("FSM.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Formation.js");
Engine.LoadComponentScript("UnitAI.js");
/* Regression test.
* Tests the FSM behaviour of a unit when walking as part of a formation,
* then exiting the formation.
* mode == 0: There is no enemy unit nearby.
* mode == 1: There is a live enemy unit nearby.
* mode == 2: There is a dead enemy unit nearby.
*/
function TestFormationExiting(mode)
{
ResetState();
var playerEntity = 5;
var unit = 10;
var enemy = 20;
var controller = 30;
AddMock(SYSTEM_ENTITY, IID_Timer, {
SetTimeout: function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
CreateActiveQuery: function(ent, minRange, maxRange, players, iid) {
return 1;
},
EnableActiveQuery: function(id) { },
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
DisableActiveQuery: function(id) { },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetPlayerByID: function(id) { return playerEntity; },
GetNumPlayers: function() { return 2; },
});
AddMock(playerEntity, IID_Player, {
GetDiplomacy: function() { return []; },
});
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false" });
AddMock(unit, IID_Position, {
GetPosition: function() { return { "x": 0, "z": 0 }; },
IsInWorld: function() { return true; },
});
AddMock(unit, IID_UnitMotion, {
GetWalkSpeed: function() { return 1; },
MoveToFormationOffset: function(target, x, z) { },
MoveToAttackRange: function(target, min, max) { },
});
AddMock(unit, IID_Vision, {
GetRange: function() { return 10; },
});
AddMock(unit, IID_Attack, {
GetRange: function() { return 10; },
GetBestAttack: function() { return "melee"; },
GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
});
unitAI.OnCreate();
unitAI.SetupRangeQuery(1);
if (mode == 1)
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 10; },
});
else if (mode == 2)
AddMock(enemy, IID_Health, {
GetHitpoints: function() { return 0; },
});
var controllerFormation = ConstructComponent(controller, "Formation");
var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true" });
AddMock(controller, IID_Position, {
JumpTo: function(x, z) { this.x = x; this.z = z; },
GetPosition: function() { return { "x": this.x, "z": this.z }; },
IsInWorld: function() { return true; },
});
AddMock(controller, IID_UnitMotion, {
SetUnitRadius: function(r) { },
SetSpeed: function(speed) { },
MoveToPoint: function(x, z) { },
});
controllerAI.OnCreate();
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
controllerFormation.SetMembers([unit]);
controllerAI.Walk(100, 100, false);
controllerAI.OnMotionChanged({ "starting": true });
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING");
controllerFormation.Disband();
if (mode == 0)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else if (mode == 1)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
else if (mode == 2)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else
TS_FAIL("invalid mode");
}
TestFormationExiting(0);
TestFormationExiting(1);
TestFormationExiting(2);

View File

@ -9,6 +9,73 @@
// plain old JS objects, with no need to serialise the complex
// FSM structure itself or to add custom serialisation code.)
/**
FSM API:
Users define the FSM behaviour like:
var FsmSpec = {
// Define some default message handlers:
"MessageName1": function(msg) {
// This function will be called in response to calls to
// Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg });
//
// In this function, 'this' is the component object passed into
// ProcessMessage, so you can access 'this.propertyName'
// and 'this.methodName()' etc.
},
"MessageName2": function(msg) {
// Another message handler.
},
// Define the behaviour for the 'STATENAME' state:
"STATENAME": {
"MessageName1": function(msg) {
// This overrides the previous MessageName1 that was
// defined earlier, and will be called instead of it
// in response to ProcessMessage.
},
// We don't override MessageName2, so the default one
// will be called instead.
// Define the 'STATENAME.SUBSTATENAME' state:
// (we support arbitrarily-nested hierarchies of states)
"SUBSTATENAME": {
"MessageName2": function(msg) {
// Override the default MessageName2.
// But we don't override MessageName1, so the one from
// STATENAME will be used instead.
},
"enter": function() {
// This is a special function called when transitioning
// into this state, or into a substate of this state.
//
// If it returns true, the transition will be aborted:
// do this if you've called SetNextState inside this enter
// handler, because otherwise the new state transition
// will get mixed up with the previous ongoing one.
// In normal cases, you can return false or nothing.
},
"leave": function() {
// Called when transitioning out of this state.
},
},
}
}
*/
function FSM(spec)
{
// The (relatively) human-readable FSM specification needs to get

View File

@ -379,6 +379,13 @@ void ForceGC(void* cbdata)
g_Console->InsertMessage(L"Garbage collection completed in: %f", time);
}
void DumpSimState(void* UNUSED(cbdata))
{
fs::wpath path (psLogDir()/L"sim_dump.txt");
std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc);
g_Game->GetSimulation2()->DumpDebugState(file);
}
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
@ -430,4 +437,5 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
scriptInterface.RegisterFunction<int, &Crash>("Crash");
scriptInterface.RegisterFunction<void, &DebugWarn>("DebugWarn");
scriptInterface.RegisterFunction<void, &ForceGC>("ForceGC");
scriptInterface.RegisterFunction<void, &DumpSimState>("DumpSimState");
}

View File

@ -51,6 +51,12 @@ public:
TS_ASSERT(componentManager->LoadScript(L"simulation/components/"+name));
}
static void Script_LoadHelperScript(void* cbdata, std::wstring name)
{
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
TS_ASSERT(componentManager->LoadScript(L"simulation/helpers/"+name));
}
void test_scripts()
{
if (!FileExists(L"simulation/components/tests/setup.js"))
@ -69,6 +75,7 @@ public:
ScriptTestSetup(componentManager.GetScriptInterface());
componentManager.GetScriptInterface().RegisterFunction<void, std::wstring, Script_LoadComponentScript> ("LoadComponentScript");
componentManager.GetScriptInterface().RegisterFunction<void, std::wstring, Script_LoadHelperScript> ("LoadHelperScript");
componentManager.LoadComponentTypes();