AI: Tactical evaluators now work on diffs instead of guessed effects
This commit is contained in:
parent
f9859c157e
commit
4e155b8556
6
TODO.md
6
TODO.md
|
@ -79,12 +79,14 @@ Artificial Intelligence
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
* Produce interesting "angle" areas
|
* Produce interesting "angle" areas
|
||||||
* AI seems unwanting to use the laser
|
* Evaluate active effects
|
||||||
* Evaluate diffs instead of 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
|
* 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
|
* Abandon fight if the AI judges there is no hope of victory
|
||||||
* Add combination of random small move and actual maneuver, as producer
|
* Add combination of random small move and actual maneuver, as producer
|
||||||
* New duel page with producers/evaluators tweaking
|
* New duel page with producers/evaluators tweaking
|
||||||
|
* Use tree techniques to account for potential future moves
|
||||||
* Prototype of evolving AI
|
* Prototype of evolving AI
|
||||||
|
|
||||||
Common UI
|
Common UI
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
canvas {
|
canvas {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jasmine-result-message {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -179,38 +179,23 @@ module TK.SpaceTac {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full list of diffs caused by applying this action
|
* 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[] {
|
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[] = [];
|
let result: BaseBattleDiff[] = [];
|
||||||
|
|
||||||
// Action usage
|
// Action usage
|
||||||
result.push(new ShipActionUsedDiff(ship, this, checked_target));
|
result.push(new ShipActionUsedDiff(ship, this, target));
|
||||||
|
|
||||||
// Power usage
|
// Power usage
|
||||||
|
let cost = this.getActionPointsUsage(ship, target);
|
||||||
if (cost) {
|
if (cost) {
|
||||||
result = result.concat(ship.getValueDiffs("power", -cost, true));
|
result = result.concat(ship.getValueDiffs("power", -cost, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action effects
|
// Action effects
|
||||||
result = result.concat(this.getSpecificDiffs(ship, battle, checked_target));
|
result = result.concat(this.getSpecificDiffs(ship, battle, target));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -224,10 +209,29 @@ module TK.SpaceTac {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the action on a battle state
|
* 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 {
|
apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean {
|
||||||
if (this.checkTarget(ship, target)) {
|
let reject = this.checkCannotBeApplied(ship);
|
||||||
let diffs = this.getDiffs(ship, battle, target);
|
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) {
|
if (diffs.length) {
|
||||||
battle.applyDiffs(diffs);
|
battle.applyDiffs(diffs);
|
||||||
return true;
|
return true;
|
||||||
|
@ -235,10 +239,6 @@ module TK.SpaceTac {
|
||||||
console.error("Could not apply action, no diff produced");
|
console.error("Could not apply action, no diff produced");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error("Could not apply action, target rejected");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,72 +1,35 @@
|
||||||
module TK.SpaceTac.Specs {
|
module TK.SpaceTac.Specs {
|
||||||
testing("Maneuver", test => {
|
testing("Maneuver", test => {
|
||||||
function compare_maneuver_effects(check: TestContext, meff1: ManeuverEffect[], meff2: ManeuverEffect[]): void {
|
test.case("uses move-fire simulation to build a list of battle diffs", check => {
|
||||||
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 => {
|
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let ship1 = battle.fleets[0].addShip();
|
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 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);
|
ship1.setArenaPosition(0, 0);
|
||||||
TestTools.setShipHP(ship1, 20, 20);
|
TestTools.setShipHP(ship1, 20, 20);
|
||||||
ship2.setArenaPosition(0, 5);
|
TestTools.setShipAP(ship1, 10);
|
||||||
TestTools.setShipHP(ship2, 30, 30);
|
ship2.setArenaPosition(500, 0);
|
||||||
ship3.setArenaPosition(0, 15);
|
TestTools.setShipHP(ship2, 70, 100);
|
||||||
TestTools.setShipHP(ship3, 30, 30);
|
ship3.setArenaPosition(560, 0);
|
||||||
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0));
|
TestTools.setShipHP(ship3, 80, 30);
|
||||||
compare_maneuver_effects(check, maneuver.effects, [
|
ship4.setArenaPosition(640, 0);
|
||||||
{ ship: ship1, effect: new DamageEffect(50), success: 1 },
|
TestTools.setShipHP(ship4, 30, 30);
|
||||||
{ ship: ship2, effect: new DamageEffect(50), success: 1 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("guesses drone effects", check => {
|
let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(530, 0));
|
||||||
let battle = new Battle();
|
check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(engine.action), Target.newFromLocation(331, 0)), "engine use");
|
||||||
let ship1 = battle.fleets[0].addShip();
|
check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power");
|
||||||
let ship2 = battle.fleets[0].addShip();
|
check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move");
|
||||||
let ship3 = battle.fleets[1].addShip();
|
check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(weapon.action), Target.newFromLocation(530, 0)), "weapon use");
|
||||||
let weapon = ship1.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
|
check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power");
|
||||||
weapon.action = new DeployDroneAction(weapon, 0, 100, 10, [new ValueEffect("shield", 10)]);
|
check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power");
|
||||||
ship1.setArenaPosition(0, 0);
|
check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value");
|
||||||
TestTools.setShipHP(ship1, 20, 20);
|
check.contains(maneuver.effects, new ShipValueDiff(ship3, "shield", -30), "ship3 shield value");
|
||||||
ship2.setArenaPosition(0, 5);
|
check.contains(maneuver.effects, new ShipValueDiff(ship3, "hull", -20), "ship3 hull value");
|
||||||
TestTools.setShipHP(ship2, 30, 30);
|
check.contains(maneuver.effects, new ShipDamageDiff(ship2, 0, 50, 50), "ship2 damage");
|
||||||
ship3.setArenaPosition(0, 15);
|
check.contains(maneuver.effects, new ShipDamageDiff(ship3, 20, 30, 50), "ship3 damage");
|
||||||
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 },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
module TK.SpaceTac {
|
module TK.SpaceTac {
|
||||||
// Single effect of a maneuver
|
|
||||||
export type ManeuverEffect = {
|
|
||||||
ship: Ship
|
|
||||||
effect: BaseEffect
|
|
||||||
success: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ship maneuver for an artifical intelligence
|
* Ship maneuver for an artifical intelligence
|
||||||
*
|
*
|
||||||
|
@ -28,7 +21,7 @@ module TK.SpaceTac {
|
||||||
simulation: MoveFireResult
|
simulation: MoveFireResult
|
||||||
|
|
||||||
// List of guessed effects of this maneuver
|
// List of guessed effects of this maneuver
|
||||||
effects: ManeuverEffect[]
|
effects: BaseBattleDiff[]
|
||||||
|
|
||||||
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) {
|
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) {
|
||||||
this.ship = ship;
|
this.ship = ship;
|
||||||
|
@ -39,7 +32,9 @@ module TK.SpaceTac {
|
||||||
let simulator = new MoveFireSimulator(this.ship);
|
let simulator = new MoveFireSimulator(this.ship);
|
||||||
this.simulation = simulator.simulateAction(this.action, this.target, move_margin);
|
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() {
|
jasmineToString() {
|
||||||
|
@ -78,37 +73,6 @@ module TK.SpaceTac {
|
||||||
return this.simulation.total_move_ap + this.simulation.total_fire_ap;
|
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.
|
* Standard feedback for this maneuver. It will apply it on the battle state.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,17 +29,13 @@ module TK.SpaceTac {
|
||||||
let dhull = 0;
|
let dhull = 0;
|
||||||
let dshield = 0;
|
let dshield = 0;
|
||||||
|
|
||||||
maneuver.effects.forEach(result => {
|
maneuver.effects.forEach(diff => {
|
||||||
if (result.ship === ship) {
|
if (diff instanceof ShipValueDiff) {
|
||||||
if (result.effect instanceof DamageEffect) {
|
if (ship.is(diff.ship_id)) {
|
||||||
let damage = result.effect.getEffectiveDamage(ship, result.success);
|
if (diff.code == "hull") {
|
||||||
dhull -= damage.hull;
|
dhull += clamp(hull + diff.diff, 0, chull) - hull;
|
||||||
dshield -= damage.shield;
|
} else if (diff.code == "shield") {
|
||||||
} else if (result.effect instanceof ValueEffect) {
|
dshield += clamp(shield + diff.diff, 0, cshield) - shield;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue