1
0
Fork 0

AI: Tactical evaluators now work on diffs instead of guessed effects

This commit is contained in:
Michaël Lemaire 2018-01-09 20:23:35 +01:00
parent f9859c157e
commit 4e155b8556
6 changed files with 73 additions and 144 deletions

View File

@ -79,12 +79,14 @@ Artificial Intelligence
-----------------------
* Produce interesting "angle" areas
* AI seems unwanting to use the laser
* Evaluate diffs instead of effects
* Evaluate active effects
* Account for luck
* Evaluators result should be more specific (final state evaluation, diff evaluation, confidence...)
* Use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
* Abandon fight if the AI judges there is no hope of victory
* Add combination of random small move and actual maneuver, as producer
* New duel page with producers/evaluators tweaking
* Use tree techniques to account for potential future moves
* Prototype of evolving AI
Common UI

View File

@ -10,6 +10,10 @@
canvas {
display: none;
}
.jasmine-result-message {
white-space: pre-wrap !important;
}
</style>
</head>

View File

@ -179,38 +179,23 @@ module TK.SpaceTac {
/**
* Get the full list of diffs caused by applying this action
*
* This does not perform any check, and assumes the action is doable
*/
getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] {
let reject = this.checkCannotBeApplied(ship);
if (reject) {
console.warn(`Action rejected - ${reject}`, ship, this, target);
return [];
}
let checked_target = this.checkTarget(ship, target);
if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target);
return [];
}
let cost = this.getActionPointsUsage(ship, checked_target);
if (ship.getValue("power") < cost) {
console.warn("Action rejected - not enough power", ship, this, checked_target);
return [];
}
let result: BaseBattleDiff[] = [];
// Action usage
result.push(new ShipActionUsedDiff(ship, this, checked_target));
result.push(new ShipActionUsedDiff(ship, this, target));
// Power usage
let cost = this.getActionPointsUsage(ship, target);
if (cost) {
result = result.concat(ship.getValueDiffs("power", -cost, true));
}
// Action effects
result = result.concat(this.getSpecificDiffs(ship, battle, checked_target));
result = result.concat(this.getSpecificDiffs(ship, battle, target));
return result;
}
@ -224,19 +209,34 @@ module TK.SpaceTac {
/**
* Apply the action on a battle state
*
* This will first check that the action can be done, then get the battle diffs and apply them.
*/
apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean {
if (this.checkTarget(ship, target)) {
let diffs = this.getDiffs(ship, battle, target);
if (diffs.length) {
battle.applyDiffs(diffs);
return true;
} else {
console.error("Could not apply action, no diff produced");
return false;
}
let reject = this.checkCannotBeApplied(ship);
if (reject) {
console.warn(`Action rejected - ${reject}`, ship, this, target);
return false;
}
let checked_target = this.checkTarget(ship, target);
if (!checked_target) {
console.warn("Action rejected - invalid target", ship, this, target);
return false;
}
let cost = this.getActionPointsUsage(ship, checked_target);
if (ship.getValue("power") < cost) {
console.warn("Action rejected - not enough power", ship, this, checked_target);
return false;
}
let diffs = this.getDiffs(ship, battle, checked_target);
if (diffs.length) {
battle.applyDiffs(diffs);
return true;
} else {
console.error("Could not apply action, target rejected");
console.error("Could not apply action, no diff produced");
return false;
}
}

View File

@ -1,72 +1,35 @@
module TK.SpaceTac.Specs {
testing("Maneuver", test => {
function compare_maneuver_effects(check: TestContext, meff1: ManeuverEffect[], meff2: ManeuverEffect[]): void {
check.equals(meff1.map(ef => ef.ship), meff2.map(ef => ef.ship), "impacted ships");
compare_effects(check, meff1.map(ef => ef.effect), meff2.map(ef => ef.effect));
check.equals(meff1.map(ef => ef.success), meff2.map(ef => ef.success), "success factor");
}
test.case("guesses weapon effects", check => {
test.case("uses move-fire simulation to build a list of battle diffs", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
let ship2 = battle.fleets[1].addShip();
let ship3 = battle.fleets[1].addShip();
let weapon = TestTools.addWeapon(ship1, 50, 0, 100, 10);
let ship4 = battle.fleets[1].addShip();
let weapon = TestTools.addWeapon(ship1, 50, 2, 200, 100);
let engine = TestTools.addEngine(ship1, 100);
ship1.setArenaPosition(0, 0);
TestTools.setShipHP(ship1, 20, 20);
ship2.setArenaPosition(0, 5);
TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0));
compare_maneuver_effects(check, maneuver.effects, [
{ ship: ship1, effect: new DamageEffect(50), success: 1 },
{ ship: ship2, effect: new DamageEffect(50), success: 1 },
]);
});
TestTools.setShipAP(ship1, 10);
ship2.setArenaPosition(500, 0);
TestTools.setShipHP(ship2, 70, 100);
ship3.setArenaPosition(560, 0);
TestTools.setShipHP(ship3, 80, 30);
ship4.setArenaPosition(640, 0);
TestTools.setShipHP(ship4, 30, 30);
test.case("guesses drone effects", check => {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
let ship3 = battle.fleets[1].addShip();
let weapon = ship1.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
weapon.action = new DeployDroneAction(weapon, 0, 100, 10, [new ValueEffect("shield", 10)]);
ship1.setArenaPosition(0, 0);
TestTools.setShipHP(ship1, 20, 20);
ship2.setArenaPosition(0, 5);
TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0));
compare_maneuver_effects(check, maneuver.effects, [
{ ship: ship1, effect: new ValueEffect("shield", 10), success: 1 },
{ ship: ship2, effect: new ValueEffect("shield", 10), success: 1 },
]);
});
test.case("guesses area effects on final location", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let engine = TestTools.addEngine(ship, 500);
let move = nn(engine.action);
TestTools.setShipAP(ship, 10);
let drone = new Drone(ship);
drone.effects = [new AttributeEffect("maneuvrability", 1)];
drone.x = 100;
drone.y = 0;
drone.radius = 50;
battle.addDrone(drone);
let maneuver = new Maneuver(ship, move, Target.newFromLocation(40, 30));
check.containing(maneuver.getFinalLocation(), { x: 40, y: 30 });
check.equals(maneuver.effects, []);
maneuver = new Maneuver(ship, move, Target.newFromLocation(100, 30));
check.containing(maneuver.getFinalLocation(), { x: 100, y: 30 });
compare_maneuver_effects(check, maneuver.effects, [
{ ship: ship, effect: new AttributeEffect("maneuvrability", 1), success: 1 },
]);
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(530, 0));
check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(engine.action), Target.newFromLocation(331, 0)), "engine use");
check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power");
check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move");
check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(weapon.action), Target.newFromLocation(530, 0)), "weapon use");
check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power");
check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power");
check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value");
check.contains(maneuver.effects, new ShipValueDiff(ship3, "shield", -30), "ship3 shield value");
check.contains(maneuver.effects, new ShipValueDiff(ship3, "hull", -20), "ship3 hull value");
check.contains(maneuver.effects, new ShipDamageDiff(ship2, 0, 50, 50), "ship2 damage");
check.contains(maneuver.effects, new ShipDamageDiff(ship3, 20, 30, 50), "ship3 damage");
});
});
}

View File

@ -1,11 +1,4 @@
module TK.SpaceTac {
// Single effect of a maneuver
export type ManeuverEffect = {
ship: Ship
effect: BaseEffect
success: number
}
/**
* Ship maneuver for an artifical intelligence
*
@ -28,7 +21,7 @@ module TK.SpaceTac {
simulation: MoveFireResult
// List of guessed effects of this maneuver
effects: ManeuverEffect[]
effects: BaseBattleDiff[]
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) {
this.ship = ship;
@ -39,7 +32,9 @@ module TK.SpaceTac {
let simulator = new MoveFireSimulator(this.ship);
this.simulation = simulator.simulateAction(this.action, this.target, move_margin);
this.effects = this.guessEffects();
this.effects = flatten(this.simulation.parts.map(part =>
part.action.getDiffs(this.ship, this.battle, part.target)
));
}
jasmineToString() {
@ -78,37 +73,6 @@ module TK.SpaceTac {
return this.simulation.total_move_ap + this.simulation.total_fire_ap;
}
/**
* Guess what will be the effects applied on any ship by this maneuver
*/
guessEffects(): ManeuverEffect[] {
let result: ManeuverEffect[] = [];
// Effects of weapon
if (this.action instanceof TriggerAction) {
this.action.getEffects(this.ship, this.target).forEach(([ship, effect, success]) => {
result.push({ ship: ship, effect: effect, success: success });
})
} else if (this.action instanceof DeployDroneAction) {
let ships = this.battle.collectShipsInCircle(this.target, this.action.drone_radius, true);
this.action.drone_effects.forEach(effect => {
result = result.concat(ships.map(ship => ({ ship: ship, effect: effect, success: 1 })));
});
}
// Area effects on final location
let location = this.getFinalLocation();
let effects = this.battle.drones.list().forEach(drone => {
if (Target.newFromLocation(location.x, location.y).isInRange(drone.x, drone.y, drone.radius)) {
result = result.concat(drone.effects.map(effect => (
{ ship: this.ship, effect: effect, success: 1 }
)));
}
});
return result;
}
/**
* Standard feedback for this maneuver. It will apply it on the battle state.
*/

View File

@ -29,17 +29,13 @@ module TK.SpaceTac {
let dhull = 0;
let dshield = 0;
maneuver.effects.forEach(result => {
if (result.ship === ship) {
if (result.effect instanceof DamageEffect) {
let damage = result.effect.getEffectiveDamage(ship, result.success);
dhull -= damage.hull;
dshield -= damage.shield;
} else if (result.effect instanceof ValueEffect) {
if (result.effect.valuetype == "hull") {
dhull = clamp(hull + result.effect.value_on, 0, chull) - hull;
} else if (result.effect.valuetype == "shield") {
dshield += clamp(shield + result.effect.value_on, 0, cshield) - shield;
maneuver.effects.forEach(diff => {
if (diff instanceof ShipValueDiff) {
if (ship.is(diff.ship_id)) {
if (diff.code == "hull") {
dhull += clamp(hull + diff.diff, 0, chull) - hull;
} else if (diff.code == "shield") {
dshield += clamp(shield + diff.diff, 0, cshield) - shield;
}
}
}