Skip to content

Pacman Bot Behavior Tree Demo

This is a demo of a behavior tree for a game playing bot. It has no practical use for Vultron, rather it's an introduction to how to use the behavior tree framework we've built.

We're providing this as an example to show how to build a behavior tree that can be used to implement some context-aware behavior.

It implements a simplified Pacman game with a bot that eats pills and avoids ghosts.

Behaviors

If no ghosts are nearby, Pacman eats one pill per tick. But if a ghost is nearby, Pacman will chase it if it is scared, otherwise he will avoid it. If he successfully avoids a ghost, he will eat a pill. The game ends when Pacman has eaten all the pills or has been eaten by a ghost.

Scoring

Scoring is as follows:

  • 10 points per pill
  • 200 points for the first ghost, 400 for the second, 800 for the third, 1600 for the fourth

There are 240 pills on the board. The max score is

\[(240 \times 10) + (200 + 400 + 800 + 1600) = 5400\]

Differences from the real thing

If the game exceeds 1000 ticks, Pacman gets bored and quits (but statistically that should never happen).

We did not model power pellets, fruit, or the maze. Ghosts just randomly get scared and then randomly stop being scared. Ghosts and pills are just counters. Ghost location is just a random "nearby" check.

The Behavior Tree

The tree structure is shown below.

graph TD
  MaybeEatPills_1["→ MaybeEatPills"]
  MaybeChaseOrAvoidGhost_2["? MaybeChaseOrAvoidGhost"]
  MaybeEatPills_1 --> MaybeChaseOrAvoidGhost_2
  NoMoreGhosts_3["#8645; NoMoreGhosts"]
  MaybeChaseOrAvoidGhost_2 --> NoMoreGhosts_3
  GhostsRemain_4(["#11052; GhostsRemain"])
  NoMoreGhosts_3 --> GhostsRemain_4
  NoGhostClose_5["#8645; NoGhostClose"]
  MaybeChaseOrAvoidGhost_2 --> NoGhostClose_5
  GhostClose_6["#127922; GhostClose"]
  NoGhostClose_5 --> GhostClose_6
  ChaseOrAvoidGhost_7["? ChaseOrAvoidGhost"]
  MaybeChaseOrAvoidGhost_2 --> ChaseOrAvoidGhost_7
  ChaseIfScared_8["→ ChaseIfScared"]
  ChaseOrAvoidGhost_7 --> ChaseIfScared_8
  GhostsScared_9(["#11052; GhostsScared"])
  ChaseIfScared_8 --> GhostsScared_9
  ChaseGhost_10["#127922; ChaseGhost"]
  ChaseIfScared_8 --> ChaseGhost_10
  CaughtGhost_11["→ CaughtGhost"]
  ChaseIfScared_8 --> CaughtGhost_11
  DecrGhostCount_12["#9648; DecrGhostCount"]
  CaughtGhost_11 --> DecrGhostCount_12
  ScoreGhost_13["#9648; ScoreGhost"]
  CaughtGhost_11 --> ScoreGhost_13
  IncrGhostScore_14["#9648; IncrGhostScore"]
  CaughtGhost_11 --> IncrGhostScore_14
  GhostsScared_15(["#11052; GhostsScared"])
  ChaseOrAvoidGhost_7 --> GhostsScared_15
  AvoidGhost_16["#127922; AvoidGhost"]
  ChaseOrAvoidGhost_7 --> AvoidGhost_16
  EatPill_17["#9648; EatPill"]
  MaybeEatPills_1 --> EatPill_17

Legend:

Symbol Meaning
? FallbackNode
SequenceNode
Invert
ActionNode
ConditionNode
🎲 Fuzzer node (randomly succeeds or fails some percentage of the time)

Demo Output

Example

# if vultron package is installed
# run the demo
$ vultrabot --pacman

# print tree and exit
$ vultrabot --pacman --print-tree

# if vultron package is not installed
$ python -m vultron.bt.base.demo.pacman

When the tree is run, it will look something like this:

=== Tick 1 ===
(>) >_MaybeEatPills_1
 | (?) ?_MaybeChaseOrAvoidGhost_2
 |  | (^) ^_NoMoreGhosts_3
 |  |  | (c) c_GhostsRemain_4
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (^) ^_NoGhostClose_5
 |  |  | (z) z_GhostClose_6
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (?) ?_ChaseOrAvoidGhost_7
 |  |  | (>) >_ChaseIfScared_8
 |  |  |  | (c) c_GhostsScared_9
 |  |  |  |  | = FAILURE
 |  |  |  | = FAILURE
 |  |  | (c) c_GhostsScared_15
 |  |  |  | = FAILURE
 |  |  | (z) z_AvoidGhost_16
 |  |  |  | = SUCCESS
 |  |  | = SUCCESS
 |  | = SUCCESS
 | (a) a_EatPill_17
 |  | = SUCCESS
 | = SUCCESS
=== Tick 2 ===
(>) >_MaybeEatPills_1
 | (?) ?_MaybeChaseOrAvoidGhost_2
 |  | (^) ^_NoMoreGhosts_3
 |  |  | (c) c_GhostsRemain_4
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (^) ^_NoGhostClose_5
 |  |  | (z) z_GhostClose_6
 |  |  |  | = FAILURE
 |  |  | = SUCCESS
 |  | = SUCCESS
 | (a) a_EatPill_17
 |  | = SUCCESS
 | = SUCCESS
=== Tick 3 ===
Ghosts are scared!
(>) >_MaybeEatPills_1
 | (?) ?_MaybeChaseOrAvoidGhost_2
 |  | (^) ^_NoMoreGhosts_3
 |  |  | (c) c_GhostsRemain_4
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (^) ^_NoGhostClose_5
 |  |  | (z) z_GhostClose_6
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (?) ?_ChaseOrAvoidGhost_7
 |  |  | (>) >_ChaseIfScared_8
 |  |  |  | (c) c_GhostsScared_9
 |  |  |  |  | = SUCCESS
 |  |  |  | (z) z_ChaseGhost_10
 |  |  |  |  | = SUCCESS
 |  |  |  | (>) >_CaughtGhost_11
 |  |  |  |  | (a) a_DecrGhostCount_12
Clyde was caught!
 |  |  |  |  |  | = SUCCESS
 |  |  |  |  | (a) a_ScoreGhost_13
 |  |  |  |  |  | = SUCCESS
 |  |  |  |  | (a) a_IncrGhostScore_14
Ghost score is now 400
 |  |  |  |  |  | = SUCCESS
 |  |  |  |  | = SUCCESS
 |  |  |  | = SUCCESS
 |  |  | = SUCCESS
 |  | = SUCCESS
 | (a) a_EatPill_17
 |  | = SUCCESS
 | = SUCCESS
=== Tick 4 ===
(>) >_MaybeEatPills_1
 | (?) ?_MaybeChaseOrAvoidGhost_2
 |  | (^) ^_NoMoreGhosts_3
 |  |  | (c) c_GhostsRemain_4
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (^) ^_NoGhostClose_5
 |  |  | (z) z_GhostClose_6
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (?) ?_ChaseOrAvoidGhost_7
 |  |  | (>) >_ChaseIfScared_8
 |  |  |  | (c) c_GhostsScared_9
 |  |  |  |  | = FAILURE
 |  |  |  | = FAILURE
 |  |  | (c) c_GhostsScared_15
 |  |  |  | = FAILURE
 |  |  | (z) z_AvoidGhost_16
 |  |  |  | = SUCCESS
 |  |  | = SUCCESS
 |  | = SUCCESS
 | (a) a_EatPill_17
 |  | = SUCCESS
 | = SUCCESS
=== Tick 5 ===
(>) >_MaybeEatPills_1
 | (?) ?_MaybeChaseOrAvoidGhost_2
 |  | (^) ^_NoMoreGhosts_3
 |  |  | (c) c_GhostsRemain_4
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (^) ^_NoGhostClose_5
 |  |  | (z) z_GhostClose_6
 |  |  |  | = SUCCESS
 |  |  | = FAILURE
 |  | (?) ?_ChaseOrAvoidGhost_7
 |  |  | (>) >_ChaseIfScared_8
 |  |  |  | (c) c_GhostsScared_9
 |  |  |  |  | = FAILURE
 |  |  |  | = FAILURE
 |  |  | (c) c_GhostsScared_15
 |  |  |  | = FAILURE
 |  |  | (z) z_AvoidGhost_16
 |  |  |  | = FAILURE
 |  |  | = FAILURE
 |  | = FAILURE
 | = FAILURE
Pacman died! He was eaten by Pinky!
Final score: 240
Ticks: 5
Dots Remaining: 236
Ghosts Remaining: 3 (Blinky, Pinky, Inky)

Demo Code

vultron.bt.base.demo.pacman

This is a demo of the bt tree library. It is a stub implementation of a bot that plays Pacman.

decr_ghost_count(obj)

decrements the ghost count

Source code in vultron/bt/base/demo/pacman.py
95
96
97
98
99
def decr_ghost_count(obj: BtNode) -> bool:
    """decrements the ghost count"""
    ghost = obj.bb.ghosts_remaining.pop()
    logger.info(f"{ghost} was caught!")
    return True

ghosts_remain(obj)

checks if there are any ghosts remaining.

Source code in vultron/bt/base/demo/pacman.py
111
112
113
def ghosts_remain(obj: BtNode) -> bool:
    """checks if there are any ghosts remaining."""
    return len(obj.bb.ghosts_remaining) > 0

ghosts_scared(obj)

checks if a ghost is scared.

Source code in vultron/bt/base/demo/pacman.py
122
123
124
def ghosts_scared(obj: BtNode) -> bool:
    """checks if a ghost is scared."""
    return obj.bb.ghosts_scared

inc_ghost_score(obj)

increments the score for the next ghost.

Source code in vultron/bt/base/demo/pacman.py
74
75
76
77
78
def inc_ghost_score(obj: BtNode) -> bool:
    """increments the score for the next ghost."""
    obj.bb.per_ghost *= GHOST_INC
    logger.info(f"Ghost score is now {obj.bb.per_ghost}")
    return True