1
0
Fork 0

Fixed several AI-related problems

This commit is contained in:
Michaël Lemaire 2017-02-16 23:59:41 +01:00
parent 13b6c13ef4
commit 9e0df7a08b
19 changed files with 163 additions and 130 deletions

7
TODO
View file

@ -1,13 +1,11 @@
* Ensure that tweens and particle emitters get destroyed once animation is done
* Ensure that tweens and particle emitters get destroyed once animation is done (or view changes)
* Highlight ships that will be included as target of current action
* Fix action tooltip sometimes not being hidden when the mouse goes out of action icon
* Do not focus on ship while targetting for area effects (dissociate hover and target)
* Active effects are not enough visible in ship list (maybe better in arena ?)
* Discrete power display, instead of the continuous power bar
* Changing active view does not cancel pending "setTimeout"s.
* Drones: add tooltip
* Drones: add hull points and take area damage
* Drones: fix not being removed when owner is in statis (owner's turn is skipped)
* More sound effects
* Add a battle log display
* Organize arena objects and information in layers
@ -20,13 +18,12 @@
* Handle effects overflowing ship tooltip when too numerous
* Proper arena scaling (not graphical, only space coordinates)
* Add a fleet evaluator to make balanced fleets
* Fix AI playing in background in GameSession.spec.ts
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator
* AI: apply safety distances to move actions
* AI: bully AI crashes when winning a battle (trying to move toward null ship!)
* AI: sometimes faces a cardinal point, then is stuck in an infinite thinking loop
* AI: use support equipments (repair drones...)
* Add a defeat screen (game over for now)
* Add a victory screen, with loot display
* Add retreat from battle

@ -1 +1 @@
Subproject commit 4e1d39b95da965bbb68900e5dd6137c6b17d5f91
Subproject commit 148a4d5e88b1956ac91ece6f180874390ca99c1e

View file

@ -103,13 +103,13 @@ module TS.SpaceTac {
expect(battle.playing_ship).toBe(ship2);
expect(battle.playing_ship_index).toBe(0);
// A dead ship is skipped
ship1.alive = false;
// A dead ship is not skipped
ship1.setDead();
battle.advanceToNextShip();
expect(battle.playing_ship).toBe(ship3);
expect(battle.playing_ship_index).toBe(2);
expect(battle.playing_ship).toBe(ship1);
expect(battle.playing_ship_index).toBe(1);
});
it("calls startTurn on ships", function () {

View file

@ -23,13 +23,13 @@ module TS.SpaceTac {
// List of deployed drones
drones: Drone[] = [];
// Boolean indicating if its the first turn
first_turn: boolean;
// Size of the battle area
width = 1000
height = 500
// Timer to use for scheduled things
timer = Timer.global;
// Create a battle between two fleets
constructor(fleet1: Fleet = null, fleet2: Fleet = null) {
this.log = new BattleLog();
@ -37,7 +37,6 @@ module TS.SpaceTac {
this.play_order = [];
this.playing_ship_index = null;
this.playing_ship = null;
this.first_turn = true;
this.ended = false;
this.fleets.forEach((fleet: Fleet) => {
@ -46,15 +45,14 @@ module TS.SpaceTac {
}
// Create a quick random battle, for testing purposes
static newQuickRandom(with_ai: boolean = false): Battle {
static newQuickRandom(start = true): Battle {
var player1 = Player.newQuickRandom("John");
var player2 = Player.newQuickRandom("Carl");
var result = new Battle(player1.fleet, player2.fleet);
if (with_ai) {
player2.ai = new BullyAI(player2.fleet);
if (start) {
result.start();
}
result.start();
return result;
}
@ -91,7 +89,6 @@ module TS.SpaceTac {
// Defines the initial ship positions of all engaged fleets
placeShips(): void {
this.first_turn = true;
this.placeFleetShips(this.fleets[0], this.width * 0.05, this.height * 0.5, 0);
this.placeFleetShips(this.fleets[1], this.width * 0.95, this.height * 0.5, Math.PI);
}
@ -173,39 +170,16 @@ module TS.SpaceTac {
this.playing_ship_index = null;
this.playing_ship = null;
} else {
var i = 0;
do {
if (this.playing_ship_index == null) {
this.playing_ship_index = 0;
} else {
this.playing_ship_index += 1;
}
if (this.playing_ship_index >= this.play_order.length) {
this.playing_ship_index = 0;
this.first_turn = false;
}
this.playing_ship = this.play_order[this.playing_ship_index];
i++;
} while (!this.playing_ship.alive && i < 1000);
if (i >= 1000) {
throw new Error("Infinite loop in advanceToNextShip");
if (this.playing_ship_index == null) {
this.playing_ship_index = 0;
} else {
this.playing_ship_index = (this.playing_ship_index + 1) % this.play_order.length;
}
this.playing_ship = this.play_order[this.playing_ship_index];
}
if (this.playing_ship) {
this.playing_ship.startTurn();
if (!this.playing_ship.isAbleToPlay()) {
// If the ship is not able to play, wait a little, then advance to the next one
setTimeout(() => {
this.playing_ship.endTurn();
this.advanceToNextShip(log);
}, 2000);
} else if (this.playing_ship.getPlayer().ai) {
// If the ship is managed by an AI, let it get to work
this.playing_ship.getPlayer().ai.playShip(this.playing_ship);
}
}
if (log) {
@ -213,6 +187,17 @@ module TS.SpaceTac {
}
}
/**
* Make an AI play the current ship
*/
playAI(ai: AbstractAI | null = null) {
if (!ai) {
// TODO Use an AI adapted to the fleet
ai = new BullyAI(this.playing_ship.fleet);
}
ai.playShip(this.playing_ship, this.timer);
}
// Start the battle
// This will call all necessary initialization steps (initiative, placement...)
// This will not add any event to the battle log

View file

@ -1,41 +1,40 @@
module TS.SpaceTac.Specs {
function applyGameSteps(session: GameSession): void {
var battle = session.getBattle();
battle.advanceToNextShip();
// TODO Make some moves (AI?)
battle.endBattle(battle.fleets[0]);
}
describe("GameSession", () => {
/**
* Compare two sessions
*/
function compare(session1: GameSession, session2: GameSession) {
expect(session1).toEqual(session2);
}
/**
* Apply deterministic game steps
*/
function applyGameSteps(session: GameSession): void {
var battle = session.getBattle();
battle.advanceToNextShip();
// TODO Make some fixed moves (AI?)
battle.endBattle(battle.fleets[0]);
}
it("serializes to a string", () => {
var session = new GameSession();
session.startQuickBattle(true);
// TODO AI sometimes starts playing in background...
session.startQuickBattle();
// Dump and reload
var dumped = session.saveToString();
var loaded_session = GameSession.loadFromString(dumped);
// Check equality
expect(loaded_session).toEqual(session);
compare(loaded_session, session);
// Apply game steps
applyGameSteps(session);
applyGameSteps(loaded_session);
// Clean stored times as they might differ
var clean = (session: GameSession) => {
session.getBattle().fleets.forEach((fleet: Fleet) => {
if (fleet.player.ai) {
fleet.player.ai.started = 0;
}
});
};
clean(session);
clean(loaded_session);
// Check equality after game steps
expect(loaded_session).toEqual(session);
compare(loaded_session, session);
});
});
}

View file

@ -42,7 +42,7 @@ module TS.SpaceTac {
// Start a new "quick battle" game
startQuickBattle(with_ai: boolean = false): void {
var battle = Battle.newQuickRandom(with_ai);
var battle = Battle.newQuickRandom();
this.player = battle.fleets[0].player;
this.player.setBattle(battle);
}

View file

@ -7,9 +7,6 @@ module TS.SpaceTac {
// Current fleet
fleet: Fleet;
// AI playing (null for human player)
ai: AbstractAI;
// List of visited star systems
visited: StarLocation[] = [];
@ -17,7 +14,6 @@ module TS.SpaceTac {
constructor(universe: Universe = new Universe()) {
this.universe = universe;
this.fleet = new Fleet(this);
this.ai = null;
}
// Create a quick random player, with a fleet, for testing purposes

View file

@ -60,7 +60,6 @@ module TS.SpaceTac {
var fleet_generator = new FleetGenerator(random);
var ship_count = random.throwInt(1, 5);
this.encounter = fleet_generator.generate(this.star.level, null, ship_count);
this.encounter.player.ai = new BullyAI(this.encounter);
}
}

View file

@ -16,6 +16,9 @@ module TS.SpaceTac {
// Random generator, if needed
random: RandomGenerator;
// Timer for scheduled calls
timer = Timer.global;
// Queue of work items to process
// Work items will be called successively, leaving time for other processing between them.
// So work items should always be as short as possible.
@ -29,23 +32,24 @@ module TS.SpaceTac {
this.random = new RandomGenerator();
}
postUnserialize(): void {
this.workqueue = [];
}
// Play a ship turn
// This will start asynchronous work. The AI will then call action methods, then advanceToNextShip to
// indicate it has finished.
playShip(ship: Ship): void {
playShip(ship: Ship, timer: Timer | null = null): void {
this.ship = ship;
this.workqueue = [];
this.started = (new Date()).getTime();
if (timer) {
this.timer = timer;
}
this.initWork();
this.processNextWorkItem();
if (this.workqueue.length > 0) {
this.processNextWorkItem();
}
}
// Add a work item to the work queue
addWorkItem(item: Function, delay: number = null): void {
addWorkItem(item: Function, delay = 100): void {
if (!this.async) {
if (item) {
item();
@ -53,19 +57,13 @@ module TS.SpaceTac {
return;
}
if (!delay) {
delay = 100;
}
var wrapped = () => {
if (item) {
item();
}
this.processNextWorkItem();
};
this.workqueue.push(() => {
setTimeout(wrapped, delay);
});
this.workqueue.push(() => this.timer.schedule(delay, wrapped));
}
// Initially fill the work queue.
@ -74,32 +72,51 @@ module TS.SpaceTac {
// Abstract method
}
/**
* Get the time spent thinking by the AI.
*/
private getDuration() {
return (new Date()).getTime() - this.started;
}
// Process the next work item
private processNextWorkItem(): void {
if (this.workqueue.length > 0) {
// Take the first item
var item = this.workqueue.shift();
item();
if (this.getDuration() >= 10000) {
console.warn("AI take too long to play, forcing turn end");
this.effectiveEndTurn();
} else {
// Take the first item
var item = this.workqueue.shift();
item();
}
} else {
this.endTurn();
}
}
// Called when we want to end the ship turn
private endTurn(): void {
if (this.async) {
var duration = (new Date()).getTime() - this.started;
if (duration < 2000) {
// Delay, as to make the AI not too fast to play
setTimeout(() => {
this.endTurn();
}, 2000 - duration);
return;
}
}
/**
* Effectively end the current ship's turn
*/
private effectiveEndTurn() {
this.ship.endTurn();
this.ship = null;
this.fleet.battle.advanceToNextShip();
}
/**
* Called when we want the AI decides to end the ship turn
*/
private endTurn(): void {
if (this.async) {
var duration = this.getDuration();
if (duration < 2000) {
// Delay, as to make the AI not too fast to play
this.timer.schedule(2000 - duration, () => this.effectiveEndTurn());
return;
}
}
this.effectiveEndTurn();
}
}
}

View file

@ -25,16 +25,19 @@ module TS.SpaceTac.Specs {
var result = ai.listAllWeapons();
expect(result.length).toBe(0);
var weapon1 = new Equipment(SlotType.Weapon);
var weapon1 = new Equipment(SlotType.Weapon, "weapon1");
weapon1.target_effects.push(new DamageEffect(50));
ai.ship.addSlot(SlotType.Weapon).attach(weapon1);
var weapon2 = new Equipment(SlotType.Weapon);
var weapon2 = new Equipment(SlotType.Weapon, "weapon2");
weapon2.target_effects.push(new DamageEffect(100));
ai.ship.addSlot(SlotType.Weapon).attach(weapon2);
var weapon3 = new Equipment(SlotType.Weapon, "weapon3");
ai.ship.addSlot(SlotType.Weapon).attach(weapon3);
ai.ship.addSlot(SlotType.Shield).attach(new Equipment(SlotType.Shield));
result = ai.listAllWeapons();
expect(result.length).toBe(2);
expect(result[0]).toBe(weapon1);
expect(result[1]).toBe(weapon2);
expect(result).toEqual([weapon1, weapon2]);
});
it("checks a firing possibility", function () {
@ -146,10 +149,12 @@ module TS.SpaceTac.Specs {
var weapon1 = new Equipment(SlotType.Weapon);
weapon1.distance = 50;
weapon1.ap_usage = 1;
weapon1.target_effects.push(new DamageEffect(10));
ai.ship.addSlot(SlotType.Weapon).attach(weapon1);
var weapon2 = new Equipment(SlotType.Weapon);
weapon2.distance = 10;
weapon2.ap_usage = 1;
weapon2.target_effects.push(new DamageEffect(5));
ai.ship.addSlot(SlotType.Weapon).attach(weapon2);
ai.ship.values.power.setMaximal(10);

View file

@ -65,7 +65,7 @@ module TS.SpaceTac {
// List all weapons
listAllWeapons(): Equipment[] {
return this.ship.listEquipment(SlotType.Weapon);
return this.ship.listEquipment(SlotType.Weapon).filter(equipement => any(equipement.target_effects, effect => effect instanceof DamageEffect));
}
// List all available maneuvers for the playing ship
@ -101,7 +101,7 @@ module TS.SpaceTac {
// Returns the BullyManeuver, or null if impossible to fire
checkBullyManeuver(enemy: Ship, weapon: Equipment): BullyManeuver {
// Check if enemy in range
var target = Target.newFromShip(enemy);
var target = weapon.blast ? Target.newFromLocation(enemy.arena_x, enemy.arena_y) : Target.newFromShip(enemy);
var distance = target.getDistanceTo(Target.newFromShip(this.ship));
var move: Target;
var engine: Equipment;

View file

@ -1,5 +1,7 @@
module TS.SpaceTac.UI {
// Base class for all game views
/**
* Base class for all game views
*/
export class BaseView extends Phaser.State {
// Link to the root UI
gameui: MainUI;
@ -10,6 +12,9 @@ module TS.SpaceTac.UI {
// Input and key bindings
inputs: InputManager;
// Timing
timer: Timer;
// Get the size of display
getWidth(): number {
return this.game.width || 1280;
@ -24,12 +29,14 @@ module TS.SpaceTac.UI {
return this.getHeight() / 2;
}
// Init the view
init(...args: any[]) {
this.gameui = <MainUI>this.game;
this.timer = new Timer();
if (this.gameui.headless) {
this.timer.makeSynchronous();
}
}
// Create view graphics
create() {
// Notifications
this.messages = new Messages(this);
@ -47,6 +54,14 @@ module TS.SpaceTac.UI {
(<any>window).view = this;
}
}
super.create();
}
shutdown() {
super.shutdown();
this.timer.cancelAll(true);
}
}
}

View file

@ -11,8 +11,10 @@ module TS.SpaceTac.UI.Specs {
var game = new MainUI(true);
spyOn(game.load, 'image').and.stub();
spyOn(game.load, 'audio').and.stub();
if (game.load) {
spyOn(game.load, 'image').and.stub();
spyOn(game.load, 'audio').and.stub();
}
if (!state) {
state = new Phaser.State();

View file

@ -147,7 +147,7 @@ module TS.SpaceTac.UI {
setShip(ship: Ship): void {
this.clearAll();
if (ship.getPlayer() === this.battleview.player) {
if (ship.getPlayer() === this.battleview.player && ship.alive) {
var actions = ship.getAvailableActions();
actions.forEach((action: BaseAction) => {
this.addAction(ship, action);

View file

@ -50,6 +50,8 @@ module TS.SpaceTac.UI {
this.ship_hovered = null;
this.log_processor = null;
this.background = null;
this.battle.timer = this.timer;
}
// Create view graphics

View file

@ -35,7 +35,7 @@ module TS.SpaceTac.UI {
delayNextEvents(duration: number) {
if (duration > 0 && !this.view.gameui.headless) {
this.delayed = true;
setTimeout(() => this.processQueued(), duration);
this.view.timer.schedule(duration, () => this.processQueued());
}
}
@ -100,7 +100,21 @@ module TS.SpaceTac.UI {
this.view.ship_list.setPlaying(event.target.ship);
this.view.action_bar.setShip(event.target.ship);
this.view.setInteractionEnabled(this.battle.canPlay(this.view.player));
if (this.battle.canPlay(this.view.player)) {
// Player turn
this.view.setInteractionEnabled(true);
} else {
this.view.setInteractionEnabled(false);
if (event.ship.isAbleToPlay()) {
// AI turn
this.battle.playAI();
} else {
// Ship unable to play, skip turn
this.view.timer.schedule(2000, () => {
this.battle.advanceToNextShip();
});
}
}
}
// Damage to ship

View file

@ -5,10 +5,14 @@ module TS.SpaceTac.UI.Specs {
inbattleview_it("handles play position of ships", (battleview: BattleView) => {
var list = battleview.ship_list;
expect(battleview.battle.play_order.length).toBe(8);
expect(list.children.length).toBe(8);
expect(list.findPlayPosition(battleview.battle.play_order[0])).toBe(0);
expect(list.findPlayPosition(battleview.battle.play_order[1])).toBe(1);
expect(list.findPlayPosition(battleview.battle.play_order[2])).toBe(2);
spyOn(battleview.battle, "playAI").and.stub();
battleview.battle.advanceToNextShip();
expect(list.findPlayPosition(battleview.battle.play_order[0])).toBe(7);

View file

@ -3,16 +3,14 @@ module TS.SpaceTac.UI {
class Message extends Phaser.Group {
text: Phaser.Text;
constructor(parent: Phaser.Group, text: string, duration: number) {
super(parent.game);
constructor(parent: Messages, text: string, duration: number) {
super(parent.container.game);
this.text = new Phaser.Text(parent.game, 0, 0, text,
this.text = new Phaser.Text(this.game, 0, 0, text,
{ font: "bold 14px Arial", fill: "#90FEE3" });
this.addChild(this.text);
setTimeout(() => {
this.hide();
}, duration);
parent.parent.timer.schedule(duration, () => this.hide());
}
// Hide the message
@ -29,10 +27,10 @@ module TS.SpaceTac.UI {
// Visual notifications of game-related messages (eg. "Game saved"...)
export class Messages {
// Link to parent view
private parent: BaseView;
parent: BaseView;
// Main group to hold the visual messages
private container: Phaser.Group;
container: Phaser.Group;
constructor(parent: BaseView) {
this.parent = parent;
@ -47,7 +45,7 @@ module TS.SpaceTac.UI {
child.y += 30;
}, this);
var message = new Message(this.container, text, duration);
var message = new Message(this, text, duration);
this.container.addChild(message);
this.parent.world.bringToTop(this.container);

View file

@ -22,7 +22,7 @@ module TS.SpaceTac.UI {
};
obj.onInputOver.add(() => {
enternext = setTimeout(enter, hovertime);
enternext = Timer.global.schedule(hovertime, enter);
hovered = true;
});
@ -34,7 +34,7 @@ module TS.SpaceTac.UI {
obj.onInputDown.add(() => {
holdstart = new Date();
enternext = setTimeout(enter, holdtime);
enternext = Timer.global.schedule(holdtime, enter);
});
obj.onInputUp.add(() => {