plan then execute
I notice that in coding Steamhammer features, I increasingly employ a pattern of separating a planning phase and an execution phase. In the Steamhammer change list a couple days ago, I described new code for ground upgrades. It’s only 78 lines, including comments and blank lines. The planning phase looks at the game and decides on a priority order for melee attack, missile attack, and carapace upgrades. The execution phase carries out the top-priority upgrades when everything needed is available. By separating the concerns, each phase has a smaller job that is easier to understand. The only cost is that you need a data structure to carry the plan between phases.
On a larger scale, the static defense controller works the same. The planning phase does not go into detail about each base, but figures out how much defense is needed for each category of base: The front line needs this many sunkens, exposed outer bases need that many, and so on. The execution phase runs the following frame, and works out the details of which specific bases need more defensive buildings, and where exactly they should be placed, and how fast they should be made. Compared to most of Steamhammer, the code is straightforward and easy to understand, and I give part of the credit to the separation of planning and execution.
On a larger scale yet is the Micro module. It accepts orders for individual units, remembers the orders, and carries them out over however many frames they take. It figures out how to kite hydras and tries to solve problems like stuck units. Micro constitutes the execution phase for individual unit micro; its job is to make life easier for the rest of the bot. It is incomplete and not as pretty as the static defense controller, but I see it as benefiting from the same general idea.
as an architectural principle
It seems to me that completely separating planning from execution at the top level of the frame loop could be a good architectural choice. onFrame() might look like this:
void PerfectBot::onFrame()
{
GameState state = collectGameState();
GamePlan plan = analyzeAndPlan(state);
execute(state, plan);
}
The planner would presumably be made up of many different modules, each planning a different aspect of play: Production, scouting, army movement, and so on. A minor point of doing all planning up front, before any execution, is that the execution phase then always sees a consistent view of the game; nothing is out of date because that module hasn’t run yet this frame. The major point is that each aspect of the planner has access to the others, so that (at least in principle) resources can be allocated well, conflicting goals can be reconciled, and tradeoffs can be resolved using all information. All this happens before the bot takes any action, so it should be easier to arrange for it to take good actions. For example, if the planner assigns each unit one job, then the bot should never have bugs where two modules both think they control the same unit (which has happened to Steamhammer).
The execution phase would presumably have many modules too, one for each executable aspect of the plan. They might be parallel to the analysis modules, but I don’t see that they have to be.
Compare CherryPi’s blackboard architecture. The blackboard is a global data structure which lets program modules communicate with each other. A blackboard is a good foundation for separating planning from execution, whether at the frame loop level or otherwise, and CherryPi uses it that way.
Comments
Bytekeeper on :
The "cost" in my opinion might be that plans mess up priorities *because* of hidden dependencies.
Ie. one example scenario where pre-planning could be a problem: One plan want to build a pylon, at an expansion location. Another one want to build a nexus. Sending two probes would just be a waste of resources.
A complex planner could remember that the probe could be used in both plans in the planning phase.
With mixed planning/execution the "second" planner could see that the probe would be free later more easily.
Both ways fails if there are enough resources to build nexus + pylon immediately.
I'm just brainstorming, but maybe plans should not assign units directly, but add a "claim" to some unit. Ie. "NeedProbe(Location,Time)" - and a separate resource planner step would see:
Claim Plan A - NeedProbe(exp1, 1000 frames)
Claim Plan B - NeedProbe(exp1, 1100 frames)
Resource Planner could
* use probe 1 for both tasks
* use probe 1 for Plan A and probe 2 for Plan B
Using probe 1: Wasted time = frames to exp1 + 100 frames
Using probe 1 and 2: Wasted time = 2 * frames to exp1
Maybe there's an easier way to resolve situations like these...
Jay Scott on :
In the case you’re considering, different sub-planners make independent plans. What they need to do is communicate, one way or another, about their resource needs. One way is to do the planning in phases: First, each planner runs independently, and part of its output is a declaration of resources needed. Then assign priorities, and divide resources. Cut out plans whose needs can’t be met.
This can also be done iteratively, to converge on a good allocation of resources.