thutson3876
PolyPuck is a game prototype made in Godot where a player battles against an advanced AI in order to break blocks until they can score against their side.
The core of this project resides in the AI the player goes against. It uses Goal Oriented Action Planning (GOAP) in order to adapt to the player and the board state.
Additionally, one bot simply isn’t enough. A branch of PolyPuck uses UDP sockets and a custom Python SDK to allow developers to set up their own machine learning bot to train in-game, and play against players or other bots.
Curious about the client/server and machine learning?
Development Tasks
- GOAP Architecture
- K-D Tree Architecture
- Advanced AI Programming
- Scalable UI
- Procedural Map Generation
Opponent AI Development

Navigation
The bot uses a navigation mesh in order to determine where it can, and can’t go. Whenever a block breaks, the mesh is updated.
Board State
The AI uses a blackboard in order to keep track of the board state. It maps strings to boolean values that are determined by a variety of variable values. These values include the puck’s position & velocity, the bot’s position, what actions it has available (dash/fireball), and a ‘mood’ variable.
The action planner then takes these states into account to determine what its desired goal should be, and which states must be satisfied in order to accomplish that goal. It compiles a set of actions that, when completed, would result in that goal’s state being achieved.
Mood
Seen in the trailer as the top-most number, representing the player’s (left) mood.
“Mood” is a culmination of two key variables: the puck’s projected path and the median health of nearby blocks. This value quantifies how safe/vulnerable either player is at any given moment.
The projected path is calculated simply by taking the velocity of the puck, multiplying it by a weight scalar, and adding it to its position vector.
The median block health required much more in order for it to be fetched efficiently. The blocks generated by the map are added to a k-dimensional tree. This allows for a very quick radial search to be performed that fetches all blocks within a given radius.
Whenever the mood needs to be fetched, a radial search around the puck’s projected path is performed in the k-d tree. The median health of the blocks within range is then normalized based on the maximum possible block health for the lobby.
This resulting value is then added to a normalized value based on how close it is to the closest goal. Since the mood value is the same regardless of side it is on, it is negative for the bot when the puck is closest to its own goal, and positive when closest to its opponent’s.

Procedural Map Generation
Grid
The grid utilizes a basic tilemap to plot out each block’s position. First, the grid takes in a preset pattern, which represents the shape of the map. A block is then generated for each tile used in the pattern. Once the left side is generated, a second grid, identical to the first, is made and then flipped and rotated around the center point so that it is an exact reflection of the first.
This process ensures that both sides are completely fair for both players. Additionally, when multiplayer is (eventually) added, this will allow the player experience to be more consistent. Each player will see their character as being on the left, regardless of how the server interprets the game sides. This effect can be seen in many digital card games such as Hearthstone.

Procedural Blocks
Originally, the map was going to be generated completely procedurally. This would have included the blocks’ health, as well as the block ‘pattern.’ However, procedurally generated maps lacked the structure needed for a fast-paced, competitive game.
The grid uses a simple noise generator to make the health of each block feel natural. Like with the grid pattern, the noise is reflected across the center so that both players’ blocks are the same.

The grid uses a simple noise generator to make the health of each block feel natural. Like with the grid pattern, the noise is reflected across the center so that both players’ blocks are the same.
The noise is then sampled based on the tile’s grid coordinates. That value is then normalized and multiplied by the maximum box health of the lobby (Box Scalar).
The Noise Curve is an optional feature that allows for more unique noise generation. The noise per tile is inputted into the curve function, and the result is used instead. This allows for easy block health manipulation, such as increasing the extremes of the generated blocks’ health.