Jump to content

Recommended Posts

Player AI seems pretty useful for testing the game, and I think we've got sufficiently stable gameplay to make it feasible to start now, so I'd like to do that. This post is an overly wordy attempt to understand and explain my initial thoughts for the design. Comments welcome :)

Currently I'm not particularly interested in the details of designing a competitive AI player in terms of strategy etc - I want to focus more on the interface design, and sort out how AI will interact with the rest of the engine, and develop a simplistic AI player to prove the system. The goal is that it should be quite straightforward for anyone to develop their own AI within the system, so we can get people trying a range of approaches (from hard-coded barely-dynamic build orders (like in Bos Wars (near the bottom)) to complex reactive algorithms).

On open source RTS games, it looks like people like writing AIs in lots of different languages (e.g. Spring has at least C, C++, Java, Python, C#). I think we should focus on only supporting JS: it's easy to distribute (just drop in the files, and it doesn't need any extra language runtimes in the engine), safe to run (we can download AIs automatically and not worry about security), fast (with JIT, and typed arrays to save memory), and can be automatically saved and loaded (for saved games and network syncing). C++ fails at distribution (AIs would have to be bundled with engine releases) and at security, and saving/loading is usually a pain, so it's probably not worth supporting that natively.

As much AI code as possible should be in scripts rather than in the engine, because script code is easier to write and more flexible and I don't think there's a good reason to do things in C++ instead.

(If it's not much extra effort, it'd be nice to support people doing AI research in other languages (as opposed to making AIs for real players to use). I think that should be fairly easy with IPC (like with the Broodwar proxy): run the AI in a separate process, have it connect to the game engine over a TCP socket, then send and receive data with some kind of JSON-based API that's equivalent to the normal internal JS<->engine interface. That means we just need to document the API and expose a socket, and people can use whatever language they fancy, and we can stay focused on JS.)

We shouldn't run all AI code as a part of the normal simulation turn update function. If the renderer is going at 60fps, but the AI spends 100ms computing stuff, it'll be noticeably jerky, so we'd have to be careful to ensure the AI never takes more than a few milliseconds per update. It seems better if we run it asynchronously, so it can be in parallel with the renderer - if it takes 100ms then that's okay since it won't delay anything. So the AI should take a read-only snapshot of the world state at turn n, then run in a background thread to compute a set of commands to execute in turn n+1. (Then we can benefit from multi-core CPUs, too.)

Output should be a set of commands exactly like what the GUI generates (via PostNetworkCommand), so we can reuse the same command-processing code and ensure the AIs don't accidentally cheat or break the game state. I think we probably want AIs to run on every player's computer in multiplayer games, rather than just running on one and sharing its command output over the network, but if AI is expensive and some players have very slow CPUs and fast networks then maybe that's a tradeoff to explore later (the design shouldn't make it impossible).

I think one main design concern is exactly what the input to the AI scripts should be. Since AIs will run in threads, and since shared-memory concurrency is evil, they won't be able to directly query the simulation system to pull details about the current state (which seems to be what most games do) - at the end of a turn we'll have to push the relevant data to them. There's potentially quite a lot of data (a hundred thousand map tiles, thousands of entities, maybe 8 AI players), so I expect we'll have to be at least a bit careful about performance.

Probably best to start from what features the AI might want to implement:

* Planning economic and military strategies - if the AI sees the enemy has lots of cavalry, it can search for counter unit types and find the spearmen and recognise it needs to build a barracks to train them. (The AI scripts shouldn't have to hardcode these details, else they'll break when we change the unit design or add mods.)

* Building placement - find free space, with various constraints (e.g. near resources, near friendly units, far from enemy units, certain distance from friendly buildings of the same type, etc).

* Resource gathering - needs to know where the resources are, so it can choose where to construct dropsites and send workers.

* Finding idle workers - needs to detect them and give them something to do.

* Terrain analysis - finding islands, hills, choke points, forests, towns, etc, for use with strategic decision-making.

* (I can't think of anything else relevant here.)

So we could do with the following data:

* Static data about entity templates: what can be trained/built, what role it can fulfil, what it will cost, what the prerequisites are (builders, buildings, phases), etc. This would basically be the collection of all entity template XML files.

* Basic game state: current resource counts, pop counts, time, list of players, diplomacy status, etc. Also a list of recent special events: defeats, tributes, chat messages, etc.

* Entity data: type, owner, location, health, stamina, current task, resource count, construction percentage, etc, for every entity in the world.

* Maybe a list of entity events: entities newly trained/built, entities destroyed, entities garrisoned, recent attacks, etc. These could mostly be derived from the complete set of entity data but that would be relatively painful and the engine already has the event data, so it's better to expose it as events.

* A grid approximation of obstructed tiles (like what the pathfinder uses), for finding free space. (We don't need the precise geometric obstruction shapes - the tile approximation is adequate and simpler, probably).

* Other terrain tile data: heights, wateriness, movement speeds, etc, for terrain analysis.

* FoW/SoD grid data, so AI can respect visibility constraints (unless it wants to cheat). Rather than doing this explicitly, it could be merged into e.g. the obstruction/terrain/entity data so we don't tell the AI about anything it can't see, but that makes it harder to share the obstruction/etc data between AI players and it's probably cleaner to keep the different data sources separate.

I think that's about all, and it doesn't sound too bad. So the basic idea is that the game engine will gather some data (entity templates) at the start of the game, and gather some other data (e.g. entity states) after every simulation turn, and gather some other data (e.g. terrain states) only when necessary (e.g. not unless it's changed, and not more than once every few seconds); and then it will send the data to the AI thread, which will process it and produce a list of commands before the start of the next turn.

Next: Need to design the precise API, and the way of writing and executing AI scripts.

Link to comment
Share on other sites

  • Replies 84
  • Created
  • Last Reply

Top Posters In This Topic

I agree with keeping as many functions as possible in the AI script, and not hidden within the engine. I can't really offer any productive information, I'm not an experienced developer, but I agree with JavaScript and can't wait for a basic AI to be implemented.

How do the existing defensive AI functions work?

Link to comment
Share on other sites

I can't say much about the technical implications, and overall it seems like a good plan. I want to ask something though: Am I right in thinking a lot of the code useful for the AI system can be useful for human players as well? I'm thinking things like the "actually-determining-which-units-are-idle" part of "finding the idle units", and collecting all of this data for the AI should help with collecting it for saving the game?

A related thought: preferably this release should focus on getting the AI system done, and programmers not working on that directly should help indirectly by mostly doing things that both the AI and the human player can benefit from. I leave it to you and the other programmers to decide/determine to what extent that is possible, but I really encourage you to find the possibilities.

Link to comment
Share on other sites

I think we probably want AIs to run on every player's computer in multiplayer games, rather than just running on one and sharing its command output over the network, but if AI is expensive and some players have very slow CPUs and fast networks then maybe that's a tradeoff to explore later (the design shouldn't make it impossible).

I don't know anything about AI in games, so I don't know how this usually implemented, but it seems for me more naturally to execute AI logic on one machine (server) and share its commands among other computers like human-player commands. In case if AI player logic will be performed on each computer this means that AI should be fully deterministic and can't have any random elements and also AI players should be synchronized with each other, right?

All others in your post looks for me understandable and reasonable :D

Edit:

A related thought: preferably this release should focus on getting the AI system done, and programmers not working on that directly should help indirectly by mostly doing things that both the AI and the human player can benefit from. I leave it to you and the other programmers to decide/determine to what extent that is possible, but I really encourage you to find the possibilities.

I will be glad to help with AI programming, but firstly we should plan what and how to implement and split this huge task to small relatively independent tasks/tickets. And I don't think that someone can do this without Philip :D Also I want to finish units promotion and fixing multiline centered text first, and for this I also need Philip's reviews :)

Edited by fcxSanya
Link to comment
Share on other sites

I will be glad to help with AI programming, but firstly we should plan what and how to implement and split this huge task to small relatively independent tasks/tickets. And I don't think that someone can do this without Philip :D Also I want to finish units promotion and fixing multiline centered text first, and for this I also need Philip's reviews :)

Agreed, my point is just that if you have to choose between two tasks and one is at least in some obscure way related to the AI I think you should choose the later. But finishing things definitely comes first, so don't take it as me saying everyone should drop everything they're working on ;) Only that I think we should get the AI system as solid as possible from the start :D

Link to comment
Share on other sites

How do the existing defensive AI functions work?
Currently we just have unit AI, where each unit independently looks for e.g. nearby enemies to attack. We'll keep this for AI players - the player AI code will handle the higher-level tasks of moving groups of units into the right places and telling them what orders to carry out, while the unit AI code handles the low-level details of moving and gathering and hitting the target etc.
Am I right in thinking a lot of the code useful for the AI system can be useful for human players as well? I'm thinking things like the "actually-determining-which-units-are-idle" part of "finding the idle units", and collecting all of this data for the AI should help with collecting it for saving the game?
Hmm, interesting - it may be useful for human players, but probably not in the way you're thinking :D. Saving already works (it just needs testing and UI), so that won't be affected. Finding idle workers is trivial and it wouldn't hurt to write that code twice. But maybe some terrain analysis stuff shouldn't be AI-only - e.g. the article in GPG3 about terrain analysis suggests using it to find forests (which may change over time) then if the player right-clicks inside a forest their units can decide to gather trees from the edge of that forest. Similarly we could use forest area detection for hiding certain units. Also it can detect shore tiles, which a player's transport ships can search for when unloading. So maybe terrain analysis is a special thing that should be part of the common simulation code, not part of the AI, so it can be used by more than just the AI. Then the input to the AI scripts won't be raw terrain data, it'll be the output of the terrain analysis (a list of forests, towns, choke points, etc, and tile data annotated with shores and islands and hills etc). That takes some flexibility away from AI scripts (they'll all be given the same post-analysis data instead of analysing it themselves), but maybe not much (we can give them most of the raw terrain data too, in case they really want to do something unique), and I think it allows the design to be simplified in some ways. So that's probably a good idea :)
I don't know anything about AI in games, so I don't know how this usually implemented, but it seems for me more naturally to execute AI logic on one machine (server) and share its commands among other computers like human-player commands. In case if AI player logic will be performed on each computer this means that AI should be fully deterministic and can't have any random elements and also AI players should be synchronized with each other, right?
AI can still use random numbers - we just need to make the random number generator synchronised between all machines (which we already do for Math.random() in the component scripts). There shouldn't be any other sources of non-determinism, so synchronisation shouldn't be any more difficult than it is for the other simulation components. Sending AI commands over the network means slightly greater latency (the AI's computed commands can't be executed until they've propagated across the network, rather than running immediately on the next turn) and greater bandwidth requirements, and makes it harder to save multiplayer games (the AI state needs to be saved, but not everyone will have been computing the AI state), and I'm not sure if there are advantages to make that worth the cost.
Link to comment
Share on other sites

Terrain analysis is expensive and preferably should only be done once regardless of number of players, but if that's handled outside of AI scripts then I think there won't be any other significant computation that ought to be shared between scripts for efficiency - it's fine if they each run totally independently.

Code should be shared between AI player implementations: some utility functions might be widely useful; some AI modules (e.g. an economy manager) might be reusable by multiple player designs; some AI scripts might simply be minor customisations of others.

We need to be able to serialise the complete AI script state (for saved games, network sync stuff, etc), which may restrict the ways in which AI scripts can be written (e.g. the state can't contain closures, and global variables should not be modified).

An attempt at a design:

Say I want to create an AI called Dummy Bot. I put all the files in a directory "mods/public/simulation/ai/dummybot/". First I create a file "dummybot.json" containing various metadata, like

{
"name": "Dummy Bot",
"description": "An AI that does nothing very interesting.",
"constructor": "DummyBotAI",
...
}

The game setup screen will search for simulation/ai/*/*.json to find the options it should offer to the player.

Then I create "dummybot.js":

function DummyBotAI(playerID)
{
// The constructor for the AI player code

// Initialise some stuff for testing:
this.playerID = playerID;
this.turn = 0;
this.suicideTurn = 10;

/*
There are some read-only global values that can be used here or later,
along the lines of:

var g_EntityTemplates = {
"units/celt_cavalry_javelinist_a": {
"Health": { "Max": "130" },
... all the other stuff from the XML file ...
},
...
};

var g_MapSettings = {
"Difficulty": 0.5, // chosen by the user; possibly could be changed in the middle of a game
"GameType": "conquest",
... all the other settings from the game setup screen etc ...
};
*/
}

DummyBotAI.prototype.HandleMessage = function(game, entities, events, terrainAnalysis)
{
/*
This gets called once per simulation turn.

'game' is like { "Time": 1.5, "Players": [ { "Resources": { "wood": 100, ... }, ... }, ... ], ... }
'entities' is like { "10": { "Template": "units/celt_cavalry_javelinist_a", "Health": 100, "Owner": 2, ... }, ... }
'events' is like [ { "Type": "EntityCreated", "Id": "10", ... }, { "Type": "PlayerDefeated", ... }, ... ]
'terrainAnalysis' is not designed yet
*/

var commands = [];

if (this.turn == this.suicideTurn)
{
// Suicide, for no particular reason
var myEntities = [];
for (var ent in entities)
if (entities[ent].Owner == this.playerID)
myEntities.push(+ent);
commands.push({"type": "delete-entities", "entities": myEntities});
}

this.turn++;

return commands;
};

The engine will load this script. It will execute something equivalent to "var ai = new DummyBotAI(1)" (based on the "constructor" specified in the .json file), and then "var commands = ai.HandleMessage(...)" each turn (for each AI player) and push the commands into the command queue for the next turn.

Now let's say we want a new improved AI player based on the old one. Create superdummybot/superdummybot.json:

{
"name": "Super Dummy Bot",
"description": "An AI that does nothing very interesting, but for longer.",
"include": ["dummybot"],
"constructor": "SuperDummyBotAI",
...
}

Then create superdummybot/superdummybot.js:

function SuperDummyBotAI(playerID)
{
// Call superclass constructor
DummyBotAI.call(this, playerID);

// Make this subclass super-strong
this.suicideTurn = SUPER_LIFETIME;
}

// Inherit superclass's methods
SuperDummyBotAI.prototype = new DummyBotAI;

and superdummybot/constants.js:

const SUPER_LIFETIME = 100;

The "include" line in the .json means that before loading any superdummybot files, the dummybot files must be loaded if they aren't already (so that "prototype = new DummyBotAI" doesn't happen before DummyBotAI was defined). (Everything just gets loaded into a single global scope, so the files can refer to each other's contents like this). Most will probably include "commonutils" or similar. An AI might have lots of .js files that define classes, which all get instantiated by the main AI class constructor. All the superdummybot/*.js files get loaded in an arbitrary order - it's rarely important to control the order they're loaded (e.g. this example works either way), and this avoids the need to make people explicitly list the order to load files.

(I'm not confident this file/module loading thing is the best way to do it, but I think it's reasonably simple and sufficiently powerful for now - it could all be changed later if we find problems.)

I think this is probably enough of a design to start implementing things and get something primitive basically working, which hopefully shouldn't take long, and then I should have a better idea of how to break down all the remaining tasks :)

Link to comment
Share on other sites

Looks good, and describes nicely how I thought it might work myself. Nice work!

The only concern I have is the mention of constants.js. If all those JS files are loaded into a global scope, constants like that would be overwritten by the next AI constants file loaded right?

So everything would need to be either in the AI json file, or in the AI namespace for anything to work without being clobbered?

Link to comment
Share on other sites

Yeah, name conflicts are an issue with this approach - that's probably the aspect I'm least confident in :)

Putting everything in the global scope seems to have a number of benefits. It simplifies serialisation of AI state - the engine can save an object's class name, then when deserialising it can look up the name in the global scope to reconstruct it correctly. I think it lets us prevent dynamic changes to global objects (which would break the serialisation system) by 'freezing' the global scope after loading (so all dynamic state must be stored in the AI player objects). It works nicely with SpiderMonkey JITs (which dislike fancy tricks with fake global scopes).

But name conflicts are bad, and I suppose they're sometimes hard to avoid, e.g. if I copy-and-paste one AI player into a new directory because I want to tweak it a bit to make a new version, then want to play the old version and new version against each other, I'd have to rename every single function and constant in the new version. We could perhaps avoid that problem by running every AI player in a completely independent JS context with their own global scopes, so we don't load both AI versions into the same scope, but that's not great for performance (we'd need to clone the input data into multiple JS contexts, and they couldn't share JIT caches, etc) so I'd prefer to avoid it until it's possible to measure that it's not a significant problem. I'm not sure what else to try, though :D

Link to comment
Share on other sites

var g_MapSettings = {

"Difficulty": 0.5, // chosen by the user; possibly could be changed in the middle of a game

In the "combat"/"skirmish" mode (I don't know how it should be called, I mean mode where you can customize your opponents) and in multiplayer with bots it can be useful to be able to set difficulty for each bot separately, like some other RTS games allow to do. In this case we can pass difficulty parameter to the bot's constructor.

In the campaign mode we can set common difficulty level for each mission and apply it to each bot.

Changing difficulty in the middle of a game not looks for me as much useful feature, because RTS game sessions not so long (probably few tens of minutes average) and if you are losing you can just start new session with different settings and/or use different strategy.

Link to comment
Share on other sites

Yeah, name conflicts are an issue with this approach - that's probably the aspect I'm least confident in :)
But like I asked, they could be avoided by adding everything to the AI class? So the only thing that'd need to be unique is the AI class name? If so, this could be easily recommended in the AI scripting guides.

i.e.

Instead of this:


MAX_HOUSES = 30;
function CustomFunction() { /* snip */ }

We recommend:

function DummyBotAI(playerID) { /* snip */ }
DummyBotAI.prototype.Max_Houses = 30;
DummyBotAI.prototype.HandleMessage = function(game, entities, events, terrainAnalysis) { /* snip */ }
DummyBotAI.prototype.CustomFunction = function....

That'd work ok, and prevent any collisions, right?

Changing difficulty in the middle of a game not looks for me as much useful feature
Agreed. Once AI are set, they shouldn't be able to be changed. However..

For me, it's very important to be able to have multiple AI's running during the game. Eventually, each AI would have a difficulty rating, based on the complexity of the algorithms it has to respond to user events.

i.e. a map with 3 AI players, one should be able to be set to easy (DummyAI), one to medium (GoodAI), and one to hard (StrongAI) or any combination. The player would choose these in the game setup screen. See Globulation 2 (Glob2) for an example on multiple AI's in a game (it's undocumented, but in Glob2, AINumbi < AICastor < AINicowar <AIWarrush).

So in summary:

Instead of a setting for difficulty, the AI themselves have a rating of difficulty based on how much they can do.

And the player can choose different AIs for a different gaming experience (as each will act differently).

Link to comment
Share on other sites

I hope AI can be changed during single player campaigns by scripts. When certain objectives are completed, the computer player(s) can change his strategies.

Anyone remember the campaign in Age of Empires II where you have to assassinate the shah ( playing as the Mongols)? When he was killed the AI became less effective in fighting you etc.

Also when you build a strong army in a skirmish or you want to test your defenses, why not slide the AI difficulty from Normal to hard... ( giving the AI some extra cheats then maybe, not to mess too much around with AI scripts.)

Link to comment
Share on other sites

In the "combat"/"skirmish" mode (I don't know how it should be called, I mean mode where you can customize your opponents) and in multiplayer with bots it can be useful to be able to set difficulty for each bot separately, like some other RTS games allow to do. In this case we can pass difficulty parameter to the bot's constructor.
Good point - will do that instead.
Changing difficulty in the middle of a game not looks for me as much useful feature, because RTS game sessions not so long (probably few tens of minutes average) and if you are losing you can just start new session with different settings and/or use different strategy.
Yeah, I suppose it's unnecessary complexity here. We should probably let players change difficulty between campaign maps, but it's not really needed to change it during a game, and AI will probably be less buggy if we avoid dynamic changes like that.
But like I asked, they could be avoided by adding everything to the AI class? So the only thing that'd need to be unique is the AI class name?
I think the problem with this is we might want classes other than the main AI class, in order to have more modular code design - e.g. an economy manager class that doesn't care about the rest of the AI, or a class representing a group of units. Making those classes be members of the AI class would require ugly syntax ("DummyBotAI.prototype.UnitGroup.prototype.Foo = function() { ... }", "new DummyBotAI.prototype.UnitGroup()", etc), and also it would be hard to share functions or constants between classes (UnitGroup.Foo would have to say "DummyBotAI.prototype.MaxHouses" instead of "this.MaxHouses"). It doesn't seem good to add that complexity to all AI scripts forever, when it's only going to help the rare cases of creating a new AI based on an old one.
Instead of a setting for difficulty, the AI themselves have a rating of difficulty based on how much they can do.

And the player can choose different AIs for a different gaming experience (as each will act differently).

For campaign maps, we want the user to have some control over difficulty, but we don't want the AI to act radically differently depending on the setting (because that's unnecessary and it makes testing harder). So I think we need some numerical difficulty setting for the AIs we use in campaigns (which will cause them to lower their pop cap, slow down production, skip some more advanced tactics, etc), and might as well support that in skirmish matches too. If we have multiple independent AIs then I agree we should let skirmish players select from those, but that should be in addition to the difficulty control for each AI.
Link to comment
Share on other sites

I hope AI can be changed during single player campaigns by scripts. When certain objectives are completed, the computer player(s) can change his strategies.

Anyone remember the campaign in Age of Empires II where you have to assassinate the shah ( playing as the Mongols)? When he was killed the AI became less effective in fighting you etc.

Wasn't that done using triggers in A0E2's scenario editor, not changing the AI. I'm not sure many games allow for the AI to be changed mid-game, and I don't think it's necessary for 0AD if it'll increase development time.

I do like your idea for testing defences, I like to turtle too sometimes :) However we could create an impenetrable wall in the middle of the map that gets destroyed once a trigger is triggered - therefore letting the entire AI army attack your fortress. I can't wait!

Link to comment
Share on other sites

I'm somewhat interested in this topic and registered just to express how appalled I am at the below abuse of a perfectly nice language :)

Then I create "dummybot.js":

function DummyBotAI(playerID)
{
// The constructor for the AI player code

// Initialise some stuff for testing:
this.playerID = playerID;
this.turn = 0;
this.suicideTurn = 10;

/*
There are some read-only global values that can be used here or later,
along the lines of:

var g_EntityTemplates = {
"units/celt_cavalry_javelinist_a": {
"Health": { "Max": "130" },
... all the other stuff from the XML file ...
},
...
};

var g_MapSettings = {
"Difficulty": 0.5, // chosen by the user; possibly could be changed in the middle of a game
"GameType": "conquest",
... all the other settings from the game setup screen etc ...
};
*/
}

DummyBotAI.prototype.HandleMessage = function(game, entities, events, terrainAnalysis)
{
/*
This gets called once per simulation turn.

'game' is like { "Time": 1.5, "Players": [ { "Resources": { "wood": 100, ... }, ... }, ... ], ... }
'entities' is like { "10": { "Template": "units/celt_cavalry_javelinist_a", "Health": 100, "Owner": 2, ... }, ... }
'events' is like [ { "Type": "EntityCreated", "Id": "10", ... }, { "Type": "PlayerDefeated", ... }, ... ]
'terrainAnalysis' is not designed yet
*/

var commands = [];

if (this.turn == this.suicideTurn)
{
// Suicide, for no particular reason
var myEntities = [];
for (var ent in entities)
if (entities[ent].Owner == this.playerID)
myEntities.push(+ent);
commands.push({"type": "delete-entities", "entities": myEntities});
}

this.turn++;

return commands;
};

The engine will load this script. It will execute something equivalent to "var ai = new DummyBotAI(1)" (based on the "constructor" specified in the .json file), and then "var commands = ai.HandleMessage(...)" each turn (for each AI player) and push the commands into the command queue for the next turn.

This is uuugly.

An empty script should be a perfectly valid AI, albeit one that does nothing.

The global object (a la 'window' in browsers or 'process' in node.js) should be a 'game' object and my window to the world (=API).

Example:


game

game.startTime
game.victoryConditions
game.players (array of Player)
game.me (my Player)
game.map (map stuff)
game.canCheat (if true, script can set unit attributes (e.g. armor), move stuff instantly, see everything...)
...

aPlayer.units (e.g. me.units for my units)
...

aUnit.build(what, where)
aUnit.work(where) // build, gather, repair ... a la right-click
aUnit.location
aUnit.attack
aUnit.armor

And OMG, the "message handling"...

There should be a ton of events defined, which I could listen to with standard addEventListener (or 'on' shortcut) / removeEventListener:


var MIN = 60*1000;

var barracks = ...;

// Between 5min and 10min, the barracks should pick a unit type and build as many as possible
setTimeout(function() {
var end = new Date+5*MIN;

var unitType = .. pick the best available unit..;
barracks.build(unitType);

barracks.on('unitBuilt', function(e) {
if (new Date < end)
this.build(e.unitType); // same unit
});
}, 5*MIN);

// At 10min, send all allies a message
setTimeout(function() {
game.allies.forEach(function(ally) {
ally.tell('My army is ready!');
});
}, 10*MIN);

Also, please don't be silly with stuff like constructors and include parameters being in the manifest. Use the manifest for name, description, author, etc. Stuff that you need when you're *NOT* loading the JS file.

Who says my AI will be written OOP and not functionally? Why do I need a constructor? Also, I'll include stuff myself, tyvm (possibly based on runtime decisions). Just offer me an api like CommonJS's require().

Please, please just don't concern yourself with what is in the script file AT ALL (name conflicts, classes...). Just run the AI script and offer a global object with an API. AI script calls the engine, not the other way around. You don't see browser vendors and node.js guys worrying about your code structure, they just give you an api.

Also, give every AI it's own JS environment. There's nothing wrong with globals, let AI devs use them if they want.

We need to be able to serialise the complete AI script state (for saved games, network sync stuff, etc), which may restrict the ways in which AI scripts can be written (e.g. the state can't contain closures, and global variables should not be modified).

Please just serialize the entire Javascript environment transparently, don't pass these limitations to the AI developer.

Link to comment
Share on other sites

Thanks, interesting thoughts :)

Please just serialize the entire Javascript environment transparently, don't pass these limitations to the AI developer.
I believe that's not possible (which is unfortunate since it causes pain throughout the rest of the design). Serializing closures requires deep access to engine internals, so it can't be done by application code. SpiderMonkey has support for XDR which seems to basically do this, but it's almost totally undocumented, and I don't trust it to provide the security and portability and deterministicity that we need. (It's designed mainly for optimising Firefox startup by caching parsed scripts, where it's okay if the cache gets invalidated by changes to the machine or to SpiderMonkey - we have stricter requirements for saved games and for network synchronisation). Also it would be inefficient since it'd be serializing all the bytecode for all the functions that are defined, instead of merely serializing the variable data. I'm not aware of any reasonable solution to these problems, other than restricting the state stored by AI scripts to the subset that can be safely serialized :D
There should be a ton of events defined, which I could listen to with standard addEventListener (or 'on' shortcut) / removeEventListener:
That's using closures as part of the script state, so the serializer can't allow it.
The global object (a la 'window' in browsers or 'process' in node.js) should be a 'game' object and my window to the world (=API).

...

Also, give every AI it's own JS environment. There's nothing wrong with globals, let AI devs use them if they want.

My goal was to avoid having multiple global scope objects in the AI system, because that's often a pain. In SpiderMonkey you need a separate JSContext per global (unless you do certain tricks which are ugly hacks and cause JIT problems and are likely to break in the future), each with their own set of standard global values like 'Array' and 'undefined'. That means you get weird behaviour when passing objects between contexts (e.g. "x instanceof Array" fails if x was an array constructed in a different context).

Maybe that's not such a big deal, though... Using a separate context per AI player would prevent name clashes between them, and if the serializer was extended it could probably support global variables (while skipping over global function definitions), so it should simplify life for AI scripters, and is probably worth the added engine complexity.

Who says my AI will be written OOP and not functionally? Why do I need a constructor?
When all AI players share a global object, we need constructors so the engine can create multiple independent instances. If they're each in independent global scopes then maybe that could be simplified.
Also, I'll include stuff myself, tyvm (possibly based on runtime decisions). Just offer me an api like CommonJS's require().
CommonJS requires that each script is run in its own local scope. It seems that's usually achieved by running in a function scope (i.e. wrap with "(function(){ ... })()") but that makes serialization impossible (we can't look inside the executed function to see what variables were declared). I suppose we could run in global scope and have a new global object (hence new context) per 'module', but that sounds a bit complex and I'm not sure we really need that much modularity. We could do everything in a single global scope and have a "include('name');" function instead of specifying includes ahead of time in the .json - I'm not sure either way is particularly better.
	aUnit.build(what, where)
aUnit.work(where) // build, gather, repair ... a la right-click
aUnit.location
aUnit.attack
aUnit.armor

I want to do as much as possible in JS instead of C++, so the API provided by the engine is very raw and low-level. My vague plan is that higher-level wrappers can and should be implemented in JS - have a script library doing something like
function Unit(entities, id) {
this.entity = entities[id];
}
Unit.prototype = {
get hp() { return this.entity.hitpoints; },
get isHurt() { return this.entity.hitpoints < this.entity.maxHitpoints; },
move: function (x, z) {
Engine.PostCommand({"type": "walk", "entities": [this.entity.id], "x": x, "z": z, "queued": false});
},
};

var u = new Unit(entities, 10);
if (u.isHurt)
u.move(100, 100);

so you can construct a Unit object based on the raw data provided by the engine, and the object implements whatever API you want without changing any C++. In practice scripts will probably want objects that represent and control squads of units, which they'll have to define themselves, so they might as well define objects for individual units too.

There should eventually be some standard wrapper library like this, which all AI scripts can use, so hopefully the ugliness of the raw API can be hidden. I don't really know what it should look like, though, and whether it will really be possible without extra engine support. Maybe the best approach is to just do the raw API for now and then experiment with it and encourage people to design higher-level APIs for it, and see what the major complaints are :D

Link to comment
Share on other sites

Ykkrosh,

V8 supports snapshotting. I think it's platform independent, not sure about security... As for size, the header file (v8/include/v8-profiler.h) says the snapshot is rougly equal to the heap size. I've written a small test to make a snapshot and it's quite easy, but I haven't managed restoring it yet. If V8 is an option, I can research this further.

However, making multiplayer games saveable only by the host solves the issues of portability, security and size too (no network transfer). I'm not sure whether this is an acceptable tradeoff. It sure is for me, but I'm biased :)

Separate contexts really seems to me as the way to go. In that case, Array's are correct to be different, since each script might have modified Array.prototype. They shouldn't be passed directly anyways. Also closures, require(), and other issues you mentioned are no longer issues.

I agree completely about the higher level API being written in Javascript, it makes perfect sense. You don't wanna be defining a bunch of proxy classes in C++. Browsers implement a lot of things in Javascript too. The question is whether you should expose the lower-level API to AI scripters? I think you shouldn't. Not only is there not much value in it (assuming you have a good higher-level API), it also makes it harder to change underlying implementation of things.

I don't think you realize what opportunity you have here. Javascript is a nice language, but what's really good about it is that everyone knows it. Combine that with a clean and simple high-level API which everyone who ever played the game instantly understands, and the barrier to entry for writing AIs/campaigns/whatever is ZERO. That means it's really easy to attract new developers. You know those school competitions where people write their AIs for a simple silly board game and their programs are then run one against the other? Imagine them writing AIs for 0 A.D :D

I'm willing to help with this Javascript API / high-level to low-level "bridge".

Link to comment
Share on other sites

I'm pretty sure V8 snapshots aren't portable, since V8 doesn't even have the concept of portable bytecode (unless it all changed recently) - everything gets compiled directly to x86/x64/ARM/MIPS machine code, so the snapshot would have to include that. (Apparently that's the case). It'll probably even vary between debug and non-debug modes on the same machine, since it's just dumping the data structures directly. Looks like a snapshot could contain arbitrary unchecked machine code, so we couldn't safely let players share saved games (which is necessary if we'll e.g. allow players to rejoin an in-progress multiplayer game - clients won't want to let the server execute arbitrary code on them). Also it seems it probably won't give precisely the same output for every user (since it'll depend on e.g. GC and allocation order, I think) so we can't use it to verify that multiplayer games stay in sync. So it won't really work for any uses :P

(Also there's no way we're rewriting all of our existing SpiderMonkey-using code to use V8 because that'd be far too much work :))

Separate contexts really seems to me as the way to go. In that case, Array's are correct to be different, since each script might have modified Array.prototype. They shouldn't be passed directly anyways.
Objects won't be passed between AI players, but they'll be passed from the engine to the AI players (to provide the list of entities etc). Those objects can be quite large (thousands of entities each with dozens of data fields), and duplicating those objects for every AI player's context would be a waste of time and memory, so that input data should be shared by all player contexts. I think that's the only place where scripts might notice that arrays don't match their own context's Array.
Also closures, require(), and other issues you mentioned are no longer issues.
Closures are still an issue - if the script does a setTimeout and then gets serialised and deserialised, there's no way we can reconstruct the timer state because we don't have enough information or enough API to set up the closure and its bindings again.
The question is whether you should expose the lower-level API to AI scripters?
Expose in what sense? It's all just scripts and anyone can edit any of it, so I'm not sure how useful it is to try hiding parts from AI scripters (and distinguishing them from AI API library script developers). Probably we should strongly encourage people (via documentation) to use the standard higher-level APIs since they're easier and more stable, but I don't know that adding artificial barriers to hide the low-level API would help and wouldn't just be unnecessary complexity.
Javascript is a nice language, but what's really good about it is that everyone knows it.
Seems like quite a few game developers actually don't know it, and are much happier with Lua ;). (E.g. some people forked the Syntensity project and are moving away from its already-implemented JS support, to Lua). But yeah, JS is pretty popular and only seems likely to grow, so it's good if we can exploit that :D
I'm willing to help with this Javascript API / high-level to low-level "bridge".
That'd be great :D. I'll try committing my currently primitive working code in the next couple of days, and then it should be possible to experiment with new designs.
Link to comment
Share on other sites

Committed some code, though it still needs a lot of work (splitting into multiple contexts, improving performance, serializing more flexibly, etc) which I'll carry on with.

To test it, start a game and click the "c" button beside an unassigned player's name in the game setup screen, then select an AI from there. (The UI needs to be redesigned to be usable, when someone has a good idea for it). Or use the command-line autostart mode, like "./pyrogenesis -quickstart -autostart=Oasis -autostart-ai=2:scaredybot" (where -autostart-ai says the player number to use (and if you say player 1 then you'll share manual control with the AI) and you can repeat -autostart-ai multiple times).

Link to comment
Share on other sites

I get this error when I try using the auto start (for whatever reason, maps don't display when I just use the menu):

ERROR: JavaScript error: simulation/components/AIInterface.js line 63
ReferenceError: IID_AIInterface is not defined
()@simulation/components/AIInterface.js:63
ERROR: JavaScript error: simulation/components/AIProxy.js line 20
ReferenceError: IID_AIProxy is not defined
()@simulation/components/AIProxy.js:20
ERROR: Failed to find file: "simulation/templates/special/pathfinder.xml"
ERROR: JavaScript error: simulation/helpers/InitGame.js line 6
ReferenceError: IID_AIManager is not defined
InitGame((void 0))@simulation/helpers/InitGame.js:6
ERROR: Failed to find file: "simulation/templates/special/pathfinder.xml"
ERROR: CXeromyces: Parse error: gaia/flora_tree_date_palm:1: Did not expect element AIProxy there
ERROR: CXeromyces: Parse error: gaia/flora_tree_date_palm:1: Element Entity has extra content: AIProxy
ERROR: RelaxNGValidator: Validation failed

And then 100+ more lines listing failure to load every object on the map.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share


×
×
  • Create New...