-
Notifications
You must be signed in to change notification settings - Fork 13
Bot Architecture in CodinGame Scala Kit
First, this article explains the rationals behind the bot architecture designed in CodinGame-Scala-Kit. Then, we demostrate this architecture style with some codes.
A bot is a computer program. It communicates with a referee system. A referee system controls two or more bots. The referee defines game rules. It distributes game state to bots, collects actions and updates the game. It continues the distribute-collect-update loop until the game is over.
The following design principles are driven by challenges in bot programming. Each of the principles aimes to tackle one problem from a given perspective. The final architecture proposition should take into account the principles.
As explained above, a bot should
- communicate with the referee system
- model the game domain such as game rules, state, action
- implement a fighting strategy
Among these concerns, the communication mechanism is strictly constrainted by the referee system. We have more flexibility on domain modeling but game rules must be respected. The strategy is the heart of the bot and it's where we can be creative. On one hand, separting these concerns allows us to distinguish the fixing and moving parts. On the other hand, it helps us to invest time and efferts on the right module.
Debugging helps developers to diagnosis a bot's behavior. However, bot is often executed in a remote server. Even it's possible to write some logs, the debugging capabilities are still limited. If we managed to replay the game in developer's local environement, all debugging issues would be resolved. Therefore, the achitecture should enable developers to write replayable codes.
In most games, one wins the game if he/she looks ahead more steps than others. Better performance leads to better result. As we know, premature optimization is root of all evil for performance tuning tasks. The proposed architecture should make benchmarking and profiling easy to setup.
A well designed game should have a large action space. A game is playable when one cannot figour out a winning strategy within a reasonable amout of time.
Given that most of time, it's extremely difficult to solve a problem analytically, we often adopt an approach where we try a possible action and check how good it is. To know if an action is a good one, we need to play what-if scenarios. In a what-if scenario, we impact an action on a given state and then assess the quality of the updated game state. Playing a what-if scenarios is called a simulation.
Inside the I/O layer, we can find the domain layer where we model the game state and action. They are pure input and output data for the bot logic. All I/O related side effects are removed.
The Bot module is a logic module that is reponsable to make action decisions upon game state change. It's where most work should be done.
Separating the concerns into I/O, Model and Logic layer helps us to meet our requirements on debugging, performance, and simulation.
- Debugging: to replay a game locally, we only need to serailize the state object
- Performance: to benchmark the performance of bot logic, we only need to provide the serialized state object
- Simulation: the simulator takes action and existing state as input, and produces an updated state
The following code examples are taken from CodinGame-Scala-Kit.
trait GameIO[Context, State, Action] {
/**
* Reads game context from the referee system. A context stores game's global information
*/
def readContext: Context
/**
* Reads current state from the referee system. A state provides information for the current turn
*/
def readState(turn: Int, context: Context): State
/**
* Writes action to the referee system
*/
def writeAction(action: Action)
}
trait GameBot[State, Action] {
/**
* Reacts to the given game state by playing one or more actions
*
* @param state current state of the game
* @return one or more actions to play
*/
def react(state: State): Action
}
trait GameAccumulator[Context, State, Action] {
/**
* Accumulates information derived from the current state into a new game context
* that will be used in the next round.
*
* In certain cases, the input state doesn't include all required information.
* These information must be calculated from historical actions and states.
* For example, it could be action cool down, observed positions in fog of war.
*
* @param context the current context which may contain historical events.
* @param state the current state
* @param action actions performed for the current round
* @return a new context accumulated with historical events including those generated from the current round
*/
def accumulate(context: Context, state: State, action: Action): Context
}
trait GameSimulator[State, Action] {
/**
* Impacts the provided action on the given state and
* produces a new state according to the defined game rule
*
* @param state the starting state
* @param action action selected based on the starting state
* @return an updated state after action impact
*/
def simulate(state: State, action: Action): State
}
For more details on how state is serialized, refer to my first post on Debugging in CodinGame-Scala-Kit
object WondevPlayerDebug {
def main(args: Array[String]): Unit = {
val bot = WondevPlayer(true)
val state = WondevState(
context = WondevContext(6, 2),
turn = 1,
heights = Map(
Pos(2, 5) -> 48, Pos(1, 5) -> 48, Pos(5, 0) -> 48, Pos(0, 2) -> -1, Pos(0, 0) -> 48),
myUnits = List(Pos(3, 0), Pos(3, 4)),
opUnits = List(Pos(3, 2), Pos(5, 1)),
legalActions = List(
MoveBuild(0, S, N), MoveBuild(0, S, NW), MoveBuild(0, S, SE), MoveBuild(0, S, SW))
bot.react(state)
}
}
Benchmarking and profiling is powered by JMH, Sbt-JMH plugin and Java Flight Recorder.
/**
* jmh:run -prof jmh.extras.JFR -i 1 -wi 1 -f1 -t1 WondevBenchmark
*/
@State(Scope.Benchmark)
class WondevBenchmark {
val bot = MinimaxPlayer
val state = WondevState(
context = WondevContext(6, 2),
turn = 1,
heights = Map(
Pos(2, 5) -> 48, Pos(1, 5) -> 48, Pos(5, 0) -> 48, Pos(0, 2) -> -1, Pos(0, 0) -> 48),
myUnits = List(Pos(3, 0), Pos(3, 4)),
opUnits = List(Pos(3, 2), Pos(5, 1)),
legalActions = List(
MoveBuild(0, S, N), MoveBuild(0, S, NW), MoveBuild(0, S, SE), MoveBuild(0, S, SW))
@Benchmark
def wondevMinimax(): Unit = {
bot.reactTo(state)
}
}
The architecture proposal is influenced by ideas in functional programming such as
- side effects isolation
- data and logic separation
Please feel free to leave your comments.
This is a footer page
This is a sidebar page