First, this article explains the rationale behind the bot architecture designed in CodinGame-Scala-Kit. Then, we demonstrate this architecture style with some code.

What‘s a bot?



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 the game state to bots, collects bot actions and updates the game. It continues the distribute-collect-update loop until the game is over.

Bot design principles

The following design principles are driven by challenges in bot programming. Each of the principles aims to tackle one problem from a given perspective. The final architecture proposition should take into account the principles.

Separation of concerns

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 constrained 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, separating these concerns allows us to distinguish the fixing and moving parts. On the other hand, it helps us to invest time and efforts on the right module.

Debugging

Debugging helps developers to diagnosis a bot’s behavior. However, bot is often executed in a remote server. Even if 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.

Performance

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.

Simulation

A well designed game should have a large action space. A game is playable when one cannot figure out a winning strategy within a reasonable amount 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.

Architecture



Input/Output

The Input/Output layer handles communication with the referee system. It reads input to state and write action to output.

State/Action

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.

Bot logic

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 serialize 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

Show me some codes

The following code examples are taken from CodinGame-Scala-Kit.

I/O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}

Bot

1
2
3
4
5
6
7
8
9
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
}

Accumulator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}

Simulation

1
2
3
4
5
6
7
8
9
10
11
12
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

}

Debugging

For more details on how state is serialized, refer to my first post on Debugging in CodinGame-Scala-Kit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

}
}

Benchmark

Benchmarking and profiling is powered by JMH, Sbt-JMH plugin and Java Flight Recorder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 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)
}

}

Conclusion

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.