Build an AI Among Us Agent Locally
Run a GPU-accelerated LLM locally to power AI agents in Among Us.
Introduction
30 Second Summary
Among Us is a social deduction game where players figure out who among them is secretly a killer. What if you could build AI agents that play alongside you, making their own decisions about who to trust and who to accuse?
In this project, you will build a terminal-based Among Us simulation powered by a local AI model running on your GPU. You'll create AI agents that move between rooms, complete tasks, discuss during emergency meetings, and vote to eject suspects.
What You'll Build
A Python game engine with 5 AI agents powered by Gemma 4 E4B, running locally via llama.cpp with GPU acceleration on your NVIDIA RTX 2050.
By the end of this project, you'll have:
- A local LLM server running Gemma 4 E4B with GPU-accelerated inference on your RTX 2050.
- A playable text-based Among Us game with rooms, tasks, kills, emergency meetings, and voting.
- An AI agent system that observes surroundings, forms suspicions, discusses during meetings, and votes strategically.
- Secret Mission: Add an AI personality system where each agent has a unique playstyle that influences their decisions and discussion style.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, functions, loops, and dictionaries. No prior experience with AI models or game development is needed.
Set Up llama.cpp with CUDA
Your AI agents need a brain. In this project, that brain is Gemma 4 E4B, a language model from Google that runs entirely on your own hardware.
To run Gemma 4 locally, you need an inference server. llama.cpp is a high-performance C++ runtime that can run language models with GPU acceleration. In this step, you'll build it from source with CUDA support, download the model, and verify a local server responds to API requests.
In this step, get ready to:
- Build llama.cpp with CUDA support for GPU-accelerated inference.
- Download the Gemma 4 E4B language model and create a server start script.
- Start the local inference server and verify the API responds.
Install build dependencies and build llama.cpp
llama.cpp compiles from source so it can target your exact GPU hardware. You need a C++ compiler, cmake for the build system, and libcurl for downloading models directly from Hugging Face.
- Press Ctrl+Alt+T to open a terminal window.
- Install the build dependencies by running this command:
sudo apt-get update && sudo apt-get install -y cmake libcurl4-openssl-dev
What does this command do?
- apt-get update refreshes your package list so Ubuntu knows about the latest versions.
- cmake is the build tool that generates the Makefiles for llama.cpp.
- libcurl4-openssl-dev provides HTTPS support so the server can download models from Hugging Face.
- Clone the llama.cpp repository to your home directory by running:
git clone https://github.com/ggml-org/llama.cpp ~/llama.cpp
- Confirm the clone worked by running:
ls ~/llama.cpp
You should see folders like tools, src, and docs listed in the output.
Clone failed or timed out?
Make sure you have git installed. Run sudo apt-get install git and try again. If your network is slow, the clone may take a minute since the repository is large.
Help me troubleshoot my git clone failing for llama.cpp on Ubuntu
Now you'll compile llama.cpp with CUDA enabled so it can offload work to your RTX 2050.
- Build llama.cpp with CUDA support by running these commands:
cd ~/llama.cpp
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j$(nproc)
What do these build commands do?
- cmake -B build -DGGML_CUDA=ON generates the build files with CUDA GPU support enabled.
- cmake --build build --config Release compiles everything in optimized Release mode.
- -j$(nproc) uses all your CPU cores to speed up compilation.
This build takes a few minutes. You'll see a stream of compilation messages scrolling past.
- Once the build finishes, verify the server binary exists by running:
ls ~/llama.cpp/build/bin/llama-server
You should see the file path printed back. This confirms the build succeeded.
Build failed with CUDA errors?
- Verify CUDA is installed by running nvcc --version. You should see version 13.0.
- If cmake can't find CUDA, check that /usr/local/cuda/bin is in your PATH.
- If you see "No CUDA toolset found", your NVIDIA driver may be out of date. Run nvidia-smi to check your driver version.
Help me fix my llama.cpp CUDA build errors on Ubuntu with RTX 2050
Download the Gemma 4 E4B model
Gemma 4 E4B is a 4.5 billion parameter model with 42 layers. The Q4_K_M quantization compresses it to about 5.4 GB while preserving reasoning quality.
Instead of downloading the file manually, you'll create a startup script that uses llama.cpp's -hf flag. This flag downloads models from Hugging Face on first run and caches them to ~/.cache/llama.cpp/.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir among-us-ai
cd among-us-ai
- Confirm the folder exists by running:
ls
You should see an empty listing since the folder is brand new.
- Create the server start script by running this command:
cat > server_start.sh << 'EOF'
#!/bin/bash
# Start the Gemma 4 E4B llama-server with partial GPU offloading
~/llama.cpp/build/bin/llama-server \
-hf ggml-org/gemma-4-E4B-it-GGUF \
-c 4096 \
-ngl 20 \
--host 127.0.0.1 --port 8080
EOF
What do these server flags mean?
- -hf ggml-org/gemma-4-E4B-it-GGUF tells the server to download the official GGUF model from Hugging Face on first run.
- -c 4096 sets the context window to 4096 tokens. This controls how much text the model can process at once.
- -ngl 20 offloads 20 of the model's 42 layers to your GPU. This keeps memory usage under your 4 GB VRAM limit while still accelerating inference.
- --host 127.0.0.1 --port 8080 makes the server listen only on your local machine at port 8080.
Why only 20 layers on the GPU?
Gemma 4 E4B has 42 layers total. The full Q4_K_M model is about 5.4 GB, which exceeds your RTX 2050's 4 GB VRAM. By offloading only 20 layers, roughly 2.5 GB goes to the GPU while the remaining layers run on your CPU and system RAM.
This is llama.cpp's CPU+GPU hybrid inference mode. You get GPU speed for the layers that fit, with the CPU handling the rest.
- Make the script executable by running:
chmod +x server_start.sh
- Confirm the script exists by running:
ls
You should see server_start.sh listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file by running cat server_start.sh to see its contents.
ⓧ I'd like to double check the full code
Here is the complete server_start.sh file:
#!/bin/bash
# Start the Gemma 4 E4B llama-server with partial GPU offloading
~/llama.cpp/build/bin/llama-server \
-hf ggml-org/gemma-4-E4B-it-GGUF \
-c 4096 \
-ngl 20 \
--host 127.0.0.1 --port 8080
Start the llama-server and verify it works
Time to bring Gemma 4 online. The first time you run the script, it downloads the full 5.4 GB model file from Hugging Face. After that, it loads from cache instantly.
- Start the server by running this command:
./server_start.sh
What should I see?
You'll first see a download progress bar as the ~5.4 GB model downloads. This may take several minutes depending on your internet speed.
Once the download finishes and the model loads, look for a message like server is listening on http://127.0.0.1:8080. This means the server is ready to accept requests.
Server not starting or download failing?
- If you see a "file not found" error, make sure you ran chmod +x server_start.sh in the previous substep.
- If the download keeps timing out, check your internet connection. The -hf flag will resume from where it left off if you restart the script.
- If you see "CUDA out of memory", reduce -ngl 20 to -ngl 15 in server_start.sh.
Help me fix my llama-server startup issues on Ubuntu with an RTX 2050
Leave that terminal running. The server needs to stay active for the rest of this project.
- Open a second terminal by pressing Ctrl+Alt+T.
- Install the openai Python package by running:
pip install openai
Why the openai package for a local server?
The llama-server exposes an OpenAI-compatible API at /v1/chat/completions. This means you can use the same openai Python package that works with ChatGPT, just pointed at your local server instead.
This makes your code portable. If you ever want to swap in a cloud model later, you only change the URL.
- Verify the server responds by sending a test request with this command:
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "gemma-4-E4B-it", "messages": [{"role": "user", "content": "Say hello in one word."}], "max_tokens": 10}'
You should see a JSON response containing the model's reply. The exact wording will vary, but as long as you get a response with a choices field, your server is working.
Getting "Connection refused"?
- Switch back to your first terminal and check that the server is still running. Look for the server is listening message.
- If the server crashed after loading, you may be out of memory. Reduce -ngl to a lower value like 15 in server_start.sh and restart.
- Make sure you're using port 8080 in the curl command and that nothing else is running on that port.
Help me fix Connection refused when testing my local llama-server
Your local LLM server is alive and responding. Next up, you'll build the Among Us game world that these AI brains will inhabit.
Build the Game World
Your local LLM server is running and ready to process prompts. Now you need a game for it to play.
In this step, you'll build the Among Us game world in Python. This means defining The Skeld's map, creating data structures for players and game state, and implementing the core mechanics (movement, tasks, kills, and emergency meetings).
In this step, get ready to:
- Define The Skeld map with rooms and their connections.
- Build player visibility and game status display functions.
- Implement task, kill, report, and win condition mechanics.
Define The Skeld map and player state
Every Among Us game takes place on a map with rooms connected by hallways. You need to represent this map as data so the AI agents can reason about where to move and who they can see.
- Create a new file called game.py in the same folder as your server_start.sh file.
- Add the imports and LLM client connection by pasting this code:
"""Among Us AI Agent - Terminal Game"""
import random
import time
from dataclasses import dataclass, field
from enum import Enum
from openai import OpenAI
# --- LLM Client ---
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
MODEL = "gemma-4-E4B-it"
What does this code do?
The openai package connects to your local llama-server through its OpenAI-compatible API. The base_url points at localhost:8080 where your server is running.
The api_key is set to "not-needed" because your local server doesn't require authentication. The MODEL constant stores the model name to use in every API call.
- Below the client setup, add the map data that defines The Skeld's rooms and connections:
# --- Game Constants ---
ROOMS = {
"Cafeteria": ["Weapons", "Admin", "Medbay", "Upper Engine"],
"Weapons": ["Cafeteria", "Navigation", "O2"],
"Navigation": ["Weapons", "O2", "Shields"],
"Shields": ["Navigation", "Communications", "Storage"],
"Communications": ["Shields", "Storage"],
"Storage": ["Communications", "Shields", "Electrical", "Lower Engine", "Admin"],
"Electrical": ["Storage", "Lower Engine"],
"Lower Engine": ["Electrical", "Storage", "Reactor", "Security"],
"Reactor": ["Lower Engine", "Security", "Upper Engine"],
"Security": ["Lower Engine", "Reactor"],
"Upper Engine": ["Reactor", "Cafeteria", "Medbay"],
"Medbay": ["Upper Engine", "Cafeteria"],
"Admin": ["Cafeteria", "Storage"],
"O2": ["Weapons", "Navigation"],
}
TASK_LOCATIONS = ["Electrical", "Medbay", "Weapons", "Navigation", "O2", "Admin", "Shields", "Reactor"]
COLORS = ["Red", "Blue", "Green", "Yellow", "Purple", "White"]
What does this code do?
The ROOMS dictionary is The Skeld's map. Each key is a room name, and its value is a list of rooms connected to it. Players can only move between connected rooms.
TASK_LOCATIONS lists the rooms where crewmates can complete tasks. COLORS defines the 6 player names. The first color (Red) is always the human player.
- Below the constants, add the enums and dataclasses that define game phases and player state:
class Phase(Enum):
FREE_ROAM = "free_roam"
MEETING = "meeting"
VOTING = "voting"
GAME_OVER = "game_over"
class Role(Enum):
CREWMATE = "Crewmate"
IMPOSTOR = "Impostor"
@dataclass
class Player:
name: str
role: Role
is_ai: bool
location: str = "Cafeteria"
alive: bool = True
tasks: list = field(default_factory=list)
tasks_done: int = 0
memory: list = field(default_factory=list)
kill_cooldown: int = 0
@dataclass
class GameState:
players: list
phase: Phase = Phase.FREE_ROAM
turn: int = 0
bodies: list = field(default_factory=list)
meeting_caller: str = ""
meeting_statements: list = field(default_factory=list)
votes: dict = field(default_factory=dict)
total_tasks: int = 0
tasks_completed: int = 0
What does this code do?
Phase tracks what part of the game loop is active. Role marks each player as either a crewmate or the impostor.
The Player dataclass holds everything about one player: where they are, whether they're alive, their task list, and a memory list that stores events they've observed. AI agents use this memory to make decisions later.
GameState holds the overall game: all players, the current phase, where bodies are, and how many tasks have been completed.
- Below the dataclasses, add the functions that initialize a new game:
def assign_tasks(player: Player) -> list:
"""Assign 3 random task locations to a crewmate."""
return random.sample(TASK_LOCATIONS, 3)
def create_game() -> GameState:
"""Initialize a new game with 1 human + 5 AI players, 1 impostor."""
players = []
impostor_index = random.randint(0, 5)
for i, color in enumerate(COLORS):
role = Role.IMPOSTOR if i == impostor_index else Role.CREWMATE
is_ai = i != 0 # Player 0 is human
player = Player(name=color, role=role, is_ai=is_ai, location="Cafeteria")
if role == Role.CREWMATE:
player.tasks = assign_tasks(player)
players.append(player)
total_tasks = sum(len(p.tasks) for p in players if p.role == Role.CREWMATE)
return GameState(players=players, total_tasks=total_tasks)
What does this code do?
assign_tasks() picks 3 random rooms from TASK_LOCATIONS for a crewmate to visit. Each crewmate gets a different random set.
create_game() sets up a fresh game with 6 players. One random player becomes the impostor. Player 0 (Red) is always the human. Everyone starts in Cafeteria, just like the real game.
- Save game.py.
- Verify the file has no syntax errors by running this command in your terminal:
python3 game.py
- You should see no output and no errors. The script defines data structures without executing anything yet.
Seeing an import error?
If you see an error about openai not being found, make sure you installed it in Step 1 with pip install openai. Run the install again if needed.
If you see a SyntaxError, check that all brackets and quotes in the ROOMS dictionary are properly closed.
help me debug this import or syntax error in game.py
Implement the game loop and movement system
Players in Among Us can only see who is in their current room. You need functions that determine visibility and display the current game state to the terminal.
- In game.py, add these visibility helper functions below create_game():
def get_players_in_room(game: GameState, room: str) -> list:
"""Get all alive players in a specific room."""
return [p for p in game.players if p.alive and p.location == room]
def get_visible_info(game: GameState, player: Player) -> str:
"""Get what a player can see from their current location."""
others_here = [p.name for p in get_players_in_room(game, player.location) if p.name != player.name]
bodies_here = [b for b in game.bodies if b["location"] == player.location]
info = f"You are in {player.location}. "
if others_here:
info += f"Players here: {', '.join(others_here)}. "
else:
info += "No one else is here. "
if bodies_here:
info += f"DEAD BODY: {', '.join(b['name'] for b in bodies_here)}! "
return info
What does this code do?
get_players_in_room() filters the player list to only alive players in a specific room. This is the foundation for visibility, kills, and witness detection.
get_visible_info() builds a text description of what a player can see: who else is in the room with them and whether there are any dead bodies. The AI agents receive this text in their prompts to make decisions.
- Below the visibility functions, add the status display that shows the human player what's happening each turn:
def display_status(game: GameState, human: Player):
"""Display current game status to the human player."""
print(f"\n{'='*50}")
print(f" TURN {game.turn} | Role: {human.role.value} | Room: {human.location}")
print(f" Tasks: {game.tasks_completed}/{game.total_tasks} | Alive: {sum(1 for p in game.players if p.alive)}/6")
print(f"{'='*50}")
visible = get_visible_info(game, human)
print(f" {visible}")
connections = ROOMS[human.location]
print(f" Exits: {', '.join(connections)}")
print()
What does this code do?
display_status() prints a formatted status bar each turn. It shows the turn number, the player's role, their current room, overall task progress, and how many players are still alive.
It also shows who else is in the room (using get_visible_info()) and which rooms the player can move to next.
- Save game.py.
- You won't see a visual change yet. The next substep adds the action mechanics that bring the game to life.
Add task, kill, and report mechanics
Among Us has four key actions: crewmates complete tasks, the impostor kills, anyone can report a body, and anyone can call an emergency meeting. All of these live in one function that resolves whatever action a player chooses.
- Below display_status(), add the action resolution function that handles all player actions:
def resolve_action(game: GameState, player: Player, action: dict):
"""Resolve a player's action and update game state."""
act = action.get("action", "")
target = action.get("target", "")
if act == "move" and target in ROOMS.get(player.location, []):
player.location = target
player.memory.append(f"Turn {game.turn}: Moved to {target}")
if player.kill_cooldown > 0:
player.kill_cooldown -= 1
elif act == "do_task" and player.role == Role.CREWMATE:
if player.tasks_done < len(player.tasks) and player.location == player.tasks[player.tasks_done]:
player.tasks_done += 1
game.tasks_completed += 1
player.memory.append(f"Turn {game.turn}: Completed task in {player.location}")
elif act == "kill" and player.role == Role.IMPOSTOR and player.kill_cooldown == 0:
victim = next((p for p in game.players if p.name == target and p.alive and p.location == player.location), None)
witnesses = [p for p in get_players_in_room(game, player.location) if p.name != player.name and p.name != target]
if victim and not witnesses:
victim.alive = False
game.bodies.append({"name": victim.name, "location": player.location})
player.kill_cooldown = 3
player.memory.append(f"Turn {game.turn}: Killed {victim.name} in {player.location}")
elif victim and witnesses:
player.memory.append(f"Turn {game.turn}: Wanted to kill {target} but {witnesses[0].name} was watching")
elif act == "report":
bodies_here = [b for b in game.bodies if b["location"] == player.location]
if bodies_here:
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} reported a body in {player.location}!")
elif act == "call_meeting":
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} called an emergency meeting!")
# Record what others see
for observer in get_players_in_room(game, player.location):
if observer.name != player.name and observer.alive:
if act == "move":
pass # They see the player arrive
observer.memory.append(f"Turn {game.turn}: Saw {player.name} in {player.location}")
What does this code do?
This function takes an action dictionary (with an action type and optional target) and resolves it against the game state. Each branch handles a different action:
- move - validates the destination is connected to the current room, then updates location.
- do_task - only works if the crewmate is in their next assigned task room.
- kill - only succeeds if the impostor is alone with the victim (no witnesses).
- report and call_meeting - trigger the meeting phase and notify all alive players.
The last loop records observations in other players' memory. This is how AI agents "see" each other moving around the map.
- Finally, add the win condition checker below resolve_action():
def check_win_condition(game: GameState) -> str | None:
"""Check if the game has ended."""
alive_crew = [p for p in game.players if p.alive and p.role == Role.CREWMATE]
alive_impostors = [p for p in game.players if p.alive and p.role == Role.IMPOSTOR]
if not alive_impostors:
return "CREWMATES WIN! The impostor was ejected!"
if len(alive_impostors) >= len(alive_crew):
return "IMPOSTOR WINS! The crew has been overwhelmed!"
if game.tasks_completed >= game.total_tasks:
return "CREWMATES WIN! All tasks completed!"
return None
What does this code do?
check_win_condition() runs after every action to see if the game is over. There are three possible endings:
- Crewmates win by voting out the impostor (no impostors alive).
- The impostor wins when impostors equal or outnumber crewmates.
- Crewmates win by completing all tasks.
It returns None if the game is still in progress.
- Save game.py.
- Verify the full game engine works by running this test command:
python3 -c "from game import create_game, check_win_condition; g = create_game(); print(f'Players: {len(g.players)}, Tasks: {g.total_tasks}'); print(f'Win check: {check_win_condition(g)}')"
- You should see output showing 6 players, a task total (around 12-15), and Win check: None confirming the game is in progress.
Seeing a NameError or ImportError?
If you see NameError: name 'Phase' is not defined, check that your Phase and Role enums appear ABOVE the dataclasses that reference them.
If you see ImportError, make sure there are no typos in your function names. The test imports create_game and check_win_condition by exact name.
help me fix this error when testing my game.py
✔️ Awesome, I've got everything!
Great. Double check you've saved game.py and the test printed 6 players with no errors.
ⓧ I'd like to double check the full code
Here's the complete game.py file at this point:
"""Among Us AI Agent - Terminal Game"""
import random
import time
from dataclasses import dataclass, field
from enum import Enum
from openai import OpenAI
# --- LLM Client ---
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
MODEL = "gemma-4-E4B-it"
# --- Game Constants ---
ROOMS = {
"Cafeteria": ["Weapons", "Admin", "Medbay", "Upper Engine"],
"Weapons": ["Cafeteria", "Navigation", "O2"],
"Navigation": ["Weapons", "O2", "Shields"],
"Shields": ["Navigation", "Communications", "Storage"],
"Communications": ["Shields", "Storage"],
"Storage": ["Communications", "Shields", "Electrical", "Lower Engine", "Admin"],
"Electrical": ["Storage", "Lower Engine"],
"Lower Engine": ["Electrical", "Storage", "Reactor", "Security"],
"Reactor": ["Lower Engine", "Security", "Upper Engine"],
"Security": ["Lower Engine", "Reactor"],
"Upper Engine": ["Reactor", "Cafeteria", "Medbay"],
"Medbay": ["Upper Engine", "Cafeteria"],
"Admin": ["Cafeteria", "Storage"],
"O2": ["Weapons", "Navigation"],
}
TASK_LOCATIONS = ["Electrical", "Medbay", "Weapons", "Navigation", "O2", "Admin", "Shields", "Reactor"]
COLORS = ["Red", "Blue", "Green", "Yellow", "Purple", "White"]
class Phase(Enum):
FREE_ROAM = "free_roam"
MEETING = "meeting"
VOTING = "voting"
GAME_OVER = "game_over"
class Role(Enum):
CREWMATE = "Crewmate"
IMPOSTOR = "Impostor"
@dataclass
class Player:
name: str
role: Role
is_ai: bool
location: str = "Cafeteria"
alive: bool = True
tasks: list = field(default_factory=list)
tasks_done: int = 0
memory: list = field(default_factory=list)
kill_cooldown: int = 0
@dataclass
class GameState:
players: list
phase: Phase = Phase.FREE_ROAM
turn: int = 0
bodies: list = field(default_factory=list)
meeting_caller: str = ""
meeting_statements: list = field(default_factory=list)
votes: dict = field(default_factory=dict)
total_tasks: int = 0
tasks_completed: int = 0
def assign_tasks(player: Player) -> list:
"""Assign 3 random task locations to a crewmate."""
return random.sample(TASK_LOCATIONS, 3)
def create_game() -> GameState:
"""Initialize a new game with 1 human + 5 AI players, 1 impostor."""
players = []
impostor_index = random.randint(0, 5)
for i, color in enumerate(COLORS):
role = Role.IMPOSTOR if i == impostor_index else Role.CREWMATE
is_ai = i != 0 # Player 0 is human
player = Player(name=color, role=role, is_ai=is_ai, location="Cafeteria")
if role == Role.CREWMATE:
player.tasks = assign_tasks(player)
players.append(player)
total_tasks = sum(len(p.tasks) for p in players if p.role == Role.CREWMATE)
return GameState(players=players, total_tasks=total_tasks)
def get_players_in_room(game: GameState, room: str) -> list:
"""Get all alive players in a specific room."""
return [p for p in game.players if p.alive and p.location == room]
def get_visible_info(game: GameState, player: Player) -> str:
"""Get what a player can see from their current location."""
others_here = [p.name for p in get_players_in_room(game, player.location) if p.name != player.name]
bodies_here = [b for b in game.bodies if b["location"] == player.location]
info = f"You are in {player.location}. "
if others_here:
info += f"Players here: {', '.join(others_here)}. "
else:
info += "No one else is here. "
if bodies_here:
info += f"DEAD BODY: {', '.join(b['name'] for b in bodies_here)}! "
return info
def resolve_action(game: GameState, player: Player, action: dict):
"""Resolve a player's action and update game state."""
act = action.get("action", "")
target = action.get("target", "")
if act == "move" and target in ROOMS.get(player.location, []):
player.location = target
player.memory.append(f"Turn {game.turn}: Moved to {target}")
if player.kill_cooldown > 0:
player.kill_cooldown -= 1
elif act == "do_task" and player.role == Role.CREWMATE:
if player.tasks_done < len(player.tasks) and player.location == player.tasks[player.tasks_done]:
player.tasks_done += 1
game.tasks_completed += 1
player.memory.append(f"Turn {game.turn}: Completed task in {player.location}")
elif act == "kill" and player.role == Role.IMPOSTOR and player.kill_cooldown == 0:
victim = next((p for p in game.players if p.name == target and p.alive and p.location == player.location), None)
witnesses = [p for p in get_players_in_room(game, player.location) if p.name != player.name and p.name != target]
if victim and not witnesses:
victim.alive = False
game.bodies.append({"name": victim.name, "location": player.location})
player.kill_cooldown = 3
player.memory.append(f"Turn {game.turn}: Killed {victim.name} in {player.location}")
elif victim and witnesses:
player.memory.append(f"Turn {game.turn}: Wanted to kill {target} but {witnesses[0].name} was watching")
elif act == "report":
bodies_here = [b for b in game.bodies if b["location"] == player.location]
if bodies_here:
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} reported a body in {player.location}!")
elif act == "call_meeting":
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} called an emergency meeting!")
# Record what others see
for observer in get_players_in_room(game, player.location):
if observer.name != player.name and observer.alive:
if act == "move":
pass # They see the player arrive
observer.memory.append(f"Turn {game.turn}: Saw {player.name} in {player.location}")
def check_win_condition(game: GameState) -> str | None:
"""Check if the game has ended."""
alive_crew = [p for p in game.players if p.alive and p.role == Role.CREWMATE]
alive_impostors = [p for p in game.players if p.alive and p.role == Role.IMPOSTOR]
if not alive_impostors:
return "CREWMATES WIN! The impostor was ejected!"
if len(alive_impostors) >= len(alive_crew):
return "IMPOSTOR WINS! The crew has been overwhelmed!"
if game.tasks_completed >= game.total_tasks:
return "CREWMATES WIN! All tasks completed!"
return None
def display_status(game: GameState, human: Player):
"""Display current game status to the human player."""
print(f"\n{'='*50}")
print(f" TURN {game.turn} | Role: {human.role.value} | Room: {human.location}")
print(f" Tasks: {game.tasks_completed}/{game.total_tasks} | Alive: {sum(1 for p in game.players if p.alive)}/6")
print(f"{'='*50}")
visible = get_visible_info(game, human)
print(f" {visible}")
connections = ROOMS[human.location]
print(f" Exits: {', '.join(connections)}")
print()
Your game world is fully built. You have a map, players, tasks, kills, reports, and win conditions. Next up, you'll give the AI agents a brain by connecting them to the Gemma 4 LLM so they can reason about what to do each turn.
Create the AI Agent Brain
Your game world has rooms, players, movement, and kill mechanics. But the AI players are still empty shells with no ability to think or act.
In this step, you'll give each AI agent a brain by connecting them to your local Gemma 4 model. Each agent will observe who's in their room, remember past events, reason about what to do next, and respond with structured decisions that the game engine can execute.
In this step, get ready to:
- Write an AI decision function that calls your local LLM with fallback logic.
- Design structured prompts that give agents game awareness and role-specific behaviour.
- Build meeting discussion and voting functions so agents can argue and vote.
Connect to the llama-server API
Your llama-server is already running and exposes an OpenAI-compatible API. The client and MODEL constants you defined earlier handle the connection. Now you need a function that uses them to ask the LLM for a game decision each turn.
- In game.py, find the get_visible_info() function.
- Add the following get_ai_decision() function directly below it, before resolve_action():
def get_ai_decision(game: GameState, player: Player) -> dict:
"""Ask the LLM to decide an action for an AI player."""
visible = get_visible_info(game, player)
connections = ROOMS[player.location]
recent_memory = player.memory[-5:] if player.memory else ["Nothing notable yet."]
if player.role == Role.CREWMATE:
remaining_tasks = [t for t in player.tasks[player.tasks_done:]]
role_info = f"You are a CREWMATE. Your remaining tasks are in: {remaining_tasks}. Complete tasks by moving to the task room and choosing 'do_task'."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'do_task' (if in a task room), 'report' (if you see a body), 'call_meeting'"
else:
role_info = "You are the IMPOSTOR. Kill crewmates without being seen. Blend in by pretending to do tasks. You can kill someone in your room if no witnesses are present."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'kill <name>' (if alone with someone), 'report' (to seem innocent), 'call_meeting'"
prompt = f"""{role_info}
Current situation: {visible}
Recent memory: {'; '.join(recent_memory)}
Turn: {game.turn}
{actions}
Respond with ONLY a JSON object: {{"action": "<action_type>", "target": "<target_if_needed>", "reason": "<brief_thought>"}}
Example: {{"action": "move", "target": "Electrical", "reason": "Need to complete my task there"}}"""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}, playing Among Us on The Skeld. Be strategic and concise. Respond ONLY with valid JSON."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=100,
)
import json
text = response.choices[0].message.content.strip()
# Extract JSON from response
if "{" in text and "}" in text:
json_str = text[text.index("{"):text.rindex("}") + 1]
return json.loads(json_str)
except Exception as e:
pass
# Fallback: random move
return {"action": "move", "target": random.choice(connections), "reason": "wandering"}
What does this code do?
This function is the core of each AI agent's turn. It gathers context, builds a prompt, sends it to the LLM, and returns a structured action.
- get_visible_info() tells the agent who's in their room and if any bodies are nearby.
- The if/else branch gives Crewmates task-focused instructions and gives the Impostor kill-focused instructions.
- client.chat.completions.create() sends the prompt to your local llama-server using the same API format as OpenAI.
- The JSON extraction logic finds the first { to last } in the response, handling cases where the LLM adds extra text around the JSON.
- If anything goes wrong (timeout, invalid JSON, network error), the fallback picks a random connected room. The game never crashes because of a bad LLM response.
- Save game.py.
Seeing a syntax error when you save?
- Check that the triple-quoted f-string (f"""...""") uses three double quotes on each end. Mismatched quotes break the entire function.
- Make sure the doubled curly braces ({{ and }}) in the JSON example are doubled. Single braces inside an f-string are interpreted as Python expressions.
- Verify the function is placed between get_visible_info() and resolve_action() with a blank line separating each function.
Help me debug a syntax error in my get_ai_decision function
Design the agent decision prompt
The prompt you just wrote is doing a lot of heavy lifting. It uses a technique called prompt engineering to constrain the LLM into producing exactly the output your game engine needs.
Why is the prompt structured this way?
The prompt follows a pattern that maximises the quality of LLM decisions for game agents. Each section has a specific purpose.
- Role info tells the agent what team it's on and what its goals are. The Impostor gets kill-focused instructions. The Crewmate gets task-focused instructions.
- Current situation gives the agent only what it can "see" right now. This prevents it from cheating with information it shouldn't have.
- Recent memory provides the last 5 events. This lets the agent reason about patterns (who keeps showing up near bodies) without overloading the context window.
- Available actions explicitly lists what the agent can do. This prevents the LLM from inventing actions that don't exist in your game.
- JSON format with example forces structured output that your code can parse. The example teaches the model the exact shape you need.
Why a fallback instead of retries?
Local LLMs running on limited VRAM can occasionally produce malformed output or time out. A random move fallback keeps the game flowing. In a turn-based game, a 3-8 second pause per AI decision is acceptable. But hanging the entire game on a retry loop would make it unplayable.
Implement the meeting discussion and voting system
In Among Us, emergency meetings are where the real social deduction happens. Players accuse each other, defend themselves, and vote to eject someone. Your AI agents need two more functions: one to generate a discussion statement and one to cast a vote.
- In game.py, add the get_ai_statement() function directly below get_ai_decision():
def get_ai_statement(game: GameState, player: Player) -> str:
"""Get an AI player's discussion statement during a meeting."""
recent_memory = player.memory[-8:] if player.memory else ["I haven't seen anything suspicious."]
prompt = f"""You are {player.name} in an Among Us emergency meeting.
Your role: {player.role.value} (NEVER reveal you are impostor if you are one!)
Your recent observations: {'; '.join(recent_memory)}
Give a SHORT statement (1-2 sentences) about who you suspect or what you saw. Be natural and strategic."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name} in Among Us. Keep responses under 30 words. Be conversational."},
{"role": "user", "content": prompt}
],
temperature=0.8,
max_tokens=60,
)
return response.choices[0].message.content.strip()
except Exception:
return "I didn't see anything suspicious."
What does this code do?
- The function pulls the last 8 memory entries (more than get_ai_decision()'s 5) because meetings are where accumulated knowledge matters most.
- The explicit instruction "NEVER reveal you are impostor" prevents the LLM from accidentally confessing. Without it, LLMs tend to be too honest.
- A higher temperature=0.8 makes statements more varied and natural-sounding. Lower temperatures would make every agent sound the same.
- max_tokens=60 keeps statements short. Long monologues would slow down meetings and feel unnatural.
- Now add the get_ai_vote() function directly below get_ai_statement():
def get_ai_vote(game: GameState, player: Player) -> str:
"""Get an AI player's vote during a meeting."""
alive_others = [p.name for p in game.players if p.alive and p.name != player.name]
recent_memory = player.memory[-8:] if player.memory else []
prompt = f"""Vote to eject someone or skip. Alive players: {alive_others}.
Your observations: {'; '.join(recent_memory)}
Who is most suspicious? Respond with ONLY a player name or 'skip'."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}. Respond with ONLY a single name from the list or 'skip'. Nothing else."},
{"role": "user", "content": prompt}
],
temperature=0.5,
max_tokens=10,
)
vote = response.choices[0].message.content.strip()
if vote in alive_others:
return vote
return "skip"
except Exception:
return "skip"
What does this code do?
- alive_others builds a list of valid vote targets. The agent can only vote for players who are still alive.
- temperature=0.5 is lower than the statement function. Votes should be more consistent and deliberate, not random.
- max_tokens=10 aggressively limits output to a single name. The system prompt reinforces this constraint.
- The validation check (if vote in alive_others) ensures the LLM returned a real player name. If it hallucinated a name that doesn't exist, the agent skips instead.
- Save game.py.
- Your file should now have three new functions between get_visible_info() and resolve_action(): get_ai_decision, get_ai_statement, and get_ai_vote.
Not sure if the functions are in the right order?
- The order matters. Make sure get_ai_decision comes first because it calls get_visible_info() which is defined above it.
- Check that you have a single blank line between each function definition. Python requires consistent spacing.
- Make sure resolve_action() is still intact below your new functions. It should not have been overwritten.
Help me check my function order in game.py
✔️ Awesome, I've got everything!
Great. Double check you've saved game.py before moving on.
ⓧ I'd like to double check the full code
Here's the complete game.py file as it should look now. Compare your code against this:
"""Among Us AI Agent - Terminal Game"""
import random
import time
from dataclasses import dataclass, field
from enum import Enum
from openai import OpenAI
# --- LLM Client ---
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
MODEL = "gemma-4-E4B-it"
# --- Game Constants ---
ROOMS = {
"Cafeteria": ["Weapons", "Admin", "Medbay", "Upper Engine"],
"Weapons": ["Cafeteria", "Navigation", "O2"],
"Navigation": ["Weapons", "O2", "Shields"],
"Shields": ["Navigation", "Communications", "Storage"],
"Communications": ["Shields", "Storage"],
"Storage": ["Communications", "Shields", "Electrical", "Lower Engine", "Admin"],
"Electrical": ["Storage", "Lower Engine"],
"Lower Engine": ["Electrical", "Storage", "Reactor", "Security"],
"Reactor": ["Lower Engine", "Security", "Upper Engine"],
"Security": ["Lower Engine", "Reactor"],
"Upper Engine": ["Reactor", "Cafeteria", "Medbay"],
"Medbay": ["Upper Engine", "Cafeteria"],
"Admin": ["Cafeteria", "Storage"],
"O2": ["Weapons", "Navigation"],
}
TASK_LOCATIONS = ["Electrical", "Medbay", "Weapons", "Navigation", "O2", "Admin", "Shields", "Reactor"]
COLORS = ["Red", "Blue", "Green", "Yellow", "Purple", "White"]
class Phase(Enum):
FREE_ROAM = "free_roam"
MEETING = "meeting"
VOTING = "voting"
GAME_OVER = "game_over"
class Role(Enum):
CREWMATE = "Crewmate"
IMPOSTOR = "Impostor"
@dataclass
class Player:
name: str
role: Role
is_ai: bool
location: str = "Cafeteria"
alive: bool = True
tasks: list = field(default_factory=list)
tasks_done: int = 0
memory: list = field(default_factory=list)
kill_cooldown: int = 0
@dataclass
class GameState:
players: list
phase: Phase = Phase.FREE_ROAM
turn: int = 0
bodies: list = field(default_factory=list)
meeting_caller: str = ""
meeting_statements: list = field(default_factory=list)
votes: dict = field(default_factory=dict)
total_tasks: int = 0
tasks_completed: int = 0
def assign_tasks(player: Player) -> list:
"""Assign 3 random task locations to a crewmate."""
return random.sample(TASK_LOCATIONS, 3)
def create_game() -> GameState:
"""Initialize a new game with 1 human + 5 AI players, 1 impostor."""
players = []
impostor_index = random.randint(0, 5)
for i, color in enumerate(COLORS):
role = Role.IMPOSTOR if i == impostor_index else Role.CREWMATE
is_ai = i != 0 # Player 0 is human
player = Player(name=color, role=role, is_ai=is_ai, location="Cafeteria")
if role == Role.CREWMATE:
player.tasks = assign_tasks(player)
players.append(player)
total_tasks = sum(len(p.tasks) for p in players if p.role == Role.CREWMATE)
return GameState(players=players, total_tasks=total_tasks)
def get_players_in_room(game: GameState, room: str) -> list:
"""Get all alive players in a specific room."""
return [p for p in game.players if p.alive and p.location == room]
def get_visible_info(game: GameState, player: Player) -> str:
"""Get what a player can see from their current location."""
others_here = [p.name for p in get_players_in_room(game, player.location) if p.name != player.name]
bodies_here = [b for b in game.bodies if b["location"] == player.location]
info = f"You are in {player.location}. "
if others_here:
info += f"Players here: {', '.join(others_here)}. "
else:
info += "No one else is here. "
if bodies_here:
info += f"DEAD BODY: {', '.join(b['name'] for b in bodies_here)}! "
return info
def get_ai_decision(game: GameState, player: Player) -> dict:
"""Ask the LLM to decide an action for an AI player."""
visible = get_visible_info(game, player)
connections = ROOMS[player.location]
recent_memory = player.memory[-5:] if player.memory else ["Nothing notable yet."]
if player.role == Role.CREWMATE:
remaining_tasks = [t for t in player.tasks[player.tasks_done:]]
role_info = f"You are a CREWMATE. Your remaining tasks are in: {remaining_tasks}. Complete tasks by moving to the task room and choosing 'do_task'."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'do_task' (if in a task room), 'report' (if you see a body), 'call_meeting'"
else:
role_info = "You are the IMPOSTOR. Kill crewmates without being seen. Blend in by pretending to do tasks. You can kill someone in your room if no witnesses are present."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'kill <name>' (if alone with someone), 'report' (to seem innocent), 'call_meeting'"
prompt = f"""{role_info}
Current situation: {visible}
Recent memory: {'; '.join(recent_memory)}
Turn: {game.turn}
{actions}
Respond with ONLY a JSON object: {{"action": "<action_type>", "target": "<target_if_needed>", "reason": "<brief_thought>"}}
Example: {{"action": "move", "target": "Electrical", "reason": "Need to complete my task there"}}"""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}, playing Among Us on The Skeld. Be strategic and concise. Respond ONLY with valid JSON."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=100,
)
import json
text = response.choices[0].message.content.strip()
# Extract JSON from response
if "{" in text and "}" in text:
json_str = text[text.index("{"):text.rindex("}") + 1]
return json.loads(json_str)
except Exception as e:
pass
# Fallback: random move
return {"action": "move", "target": random.choice(connections), "reason": "wandering"}
def get_ai_statement(game: GameState, player: Player) -> str:
"""Get an AI player's discussion statement during a meeting."""
recent_memory = player.memory[-8:] if player.memory else ["I haven't seen anything suspicious."]
prompt = f"""You are {player.name} in an Among Us emergency meeting.
Your role: {player.role.value} (NEVER reveal you are impostor if you are one!)
Your recent observations: {'; '.join(recent_memory)}
Give a SHORT statement (1-2 sentences) about who you suspect or what you saw. Be natural and strategic."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name} in Among Us. Keep responses under 30 words. Be conversational."},
{"role": "user", "content": prompt}
],
temperature=0.8,
max_tokens=60,
)
return response.choices[0].message.content.strip()
except Exception:
return "I didn't see anything suspicious."
def get_ai_vote(game: GameState, player: Player) -> str:
"""Get an AI player's vote during a meeting."""
alive_others = [p.name for p in game.players if p.alive and p.name != player.name]
recent_memory = player.memory[-8:] if player.memory else []
prompt = f"""Vote to eject someone or skip. Alive players: {alive_others}.
Your observations: {'; '.join(recent_memory)}
Who is most suspicious? Respond with ONLY a player name or 'skip'."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}. Respond with ONLY a single name from the list or 'skip'. Nothing else."},
{"role": "user", "content": prompt}
],
temperature=0.5,
max_tokens=10,
)
vote = response.choices[0].message.content.strip()
if vote in alive_others:
return vote
return "skip"
except Exception:
return "skip"
def resolve_action(game: GameState, player: Player, action: dict):
"""Resolve a player's action and update game state."""
act = action.get("action", "")
target = action.get("target", "")
if act == "move" and target in ROOMS.get(player.location, []):
player.location = target
player.memory.append(f"Turn {game.turn}: Moved to {target}")
if player.kill_cooldown > 0:
player.kill_cooldown -= 1
elif act == "do_task" and player.role == Role.CREWMATE:
if player.tasks_done < len(player.tasks) and player.location == player.tasks[player.tasks_done]:
player.tasks_done += 1
game.tasks_completed += 1
player.memory.append(f"Turn {game.turn}: Completed task in {player.location}")
elif act == "kill" and player.role == Role.IMPOSTOR and player.kill_cooldown == 0:
victim = next((p for p in game.players if p.name == target and p.alive and p.location == player.location), None)
witnesses = [p for p in get_players_in_room(game, player.location) if p.name != player.name and p.name != target]
if victim and not witnesses:
victim.alive = False
game.bodies.append({"name": victim.name, "location": player.location})
player.kill_cooldown = 3
player.memory.append(f"Turn {game.turn}: Killed {victim.name} in {player.location}")
elif victim and witnesses:
player.memory.append(f"Turn {game.turn}: Wanted to kill {target} but {witnesses[0].name} was watching")
elif act == "report":
bodies_here = [b for b in game.bodies if b["location"] == player.location]
if bodies_here:
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} reported a body in {player.location}!")
elif act == "call_meeting":
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} called an emergency meeting!")
# Record what others see
for observer in get_players_in_room(game, player.location):
if observer.name != player.name and observer.alive:
if act == "move":
pass # They see the player arrive
observer.memory.append(f"Turn {game.turn}: Saw {player.name} in {player.location}")
def check_win_condition(game: GameState) -> str | None:
"""Check if the game has ended."""
alive_crew = [p for p in game.players if p.alive and p.role == Role.CREWMATE]
alive_impostors = [p for p in game.players if p.alive and p.role == Role.IMPOSTOR]
if not alive_impostors:
return "CREWMATES WIN! The impostor was ejected!"
if len(alive_impostors) >= len(alive_crew):
return "IMPOSTOR WINS! The crew has been overwhelmed!"
if game.tasks_completed >= game.total_tasks:
return "CREWMATES WIN! All tasks completed!"
return None
def display_status(game: GameState, human: Player):
"""Display current game status to the human player."""
print(f"\n{'='*50}")
print(f" TURN {game.turn} | Role: {human.role.value} | Room: {human.location}")
print(f" Tasks: {game.tasks_completed}/{game.total_tasks} | Alive: {sum(1 for p in game.players if p.alive)}/6")
print(f"{'='*50}")
visible = get_visible_info(game, human)
print(f" {visible}")
connections = ROOMS[human.location]
print(f" Exits: {', '.join(connections)}")
print()
Your AI agents now have a working brain. They can observe their surroundings, reason about what to do, speak during meetings, and cast votes. Next up, you'll wire these functions into the game loop and build the terminal interface so you can actually play against them.
Wire It All Together
You have a game world with rooms, tasks, and kill mechanics. You have AI agents that can reason, discuss, and vote. Now it's time to connect them into a playable game.
In this step, you'll build the terminal interface that lets you play alongside the AI, then wire the entire system into a game loop that runs until someone wins.
In this step, get ready to:
- Build a terminal input system that lets you take actions each turn.
- Create the meeting discussion and voting interface with AI thinking indicators.
- Wire everything into a main game loop and play a full game.
Integrate AI decisions into the game loop
The AI agents already know how to decide, speak, and vote. The human player needs the same interface. The get_human_action() function presents available actions and parses your terminal input into the same dictionary format the AI uses.
- In game.py, find the display_status function at the bottom of your file.
- Add the following get_human_action function below it:
def get_human_action(game: GameState, human: Player) -> dict:
"""Get the human player's action via terminal input."""
connections = ROOMS[human.location]
print(" Your actions:")
print(f" move <room> - Move to: {', '.join(connections)}")
if human.role == Role.CREWMATE and human.tasks_done < len(human.tasks):
next_task = human.tasks[human.tasks_done]
print(f" do_task - Complete task (next task in: {next_task})")
if human.role == Role.IMPOSTOR:
others_here = [p.name for p in get_players_in_room(game, human.location) if p.name != human.name]
if others_here:
print(f" kill <name> - Kill: {', '.join(others_here)}")
bodies_here = [b for b in game.bodies if b["location"] == human.location]
if bodies_here:
print(f" report - Report body!")
print(f" call_meeting - Call emergency meeting")
print()
while True:
cmd = input(" > ").strip().lower().split()
if not cmd:
continue
if cmd[0] == "move" and len(cmd) >= 2:
room = " ".join(cmd[1:]).title()
if room in connections:
return {"action": "move", "target": room, "reason": "player choice"}
print(f" Can't move there. Options: {connections}")
elif cmd[0] == "do_task":
return {"action": "do_task", "target": "", "reason": "player choice"}
elif cmd[0] == "kill" and len(cmd) >= 2:
return {"action": "kill", "target": cmd[1].title(), "reason": "player choice"}
elif cmd[0] == "report":
return {"action": "report", "target": "", "reason": "player choice"}
elif cmd[0] == "call_meeting":
return {"action": "call_meeting", "target": "", "reason": "player choice"}
else:
print(" Invalid command. Try again.")
What does this code do?
This function prints a context-sensitive action menu based on your role and location. If you're a crewmate in your task room, you see do_task. If you're the impostor with someone alone, you see kill.
The while True loop keeps prompting until you enter a valid command. It returns the same {"action": ..., "target": ...} dictionary format that the AI agents produce, so resolve_action() can process both human and AI actions identically.
- Save game.py.
Why the same dictionary format?
Both human and AI players return {"action": "...", "target": "...", "reason": "..."}. This means the game engine treats all players the same way. It doesn't care whether the decision came from an LLM or a keyboard.
Build the terminal interface for meetings
Meetings are where the social deduction happens. Each AI player thinks (with a visible loading indicator), speaks their mind, and then everyone votes. This is the most LLM-intensive part of the game.
- Below the get_human_action function you just added, add the run_meeting function:
def run_meeting(game: GameState):
"""Run the discussion and voting phase."""
print(f"\n{'🚨'*20}")
print(f" EMERGENCY MEETING called by {game.meeting_caller}!")
print(f"{'🚨'*20}\n")
alive_players = [p for p in game.players if p.alive]
human = game.players[0]
# Discussion phase
print(" --- DISCUSSION ---\n")
for player in alive_players:
if player.is_ai:
print(f" 💭 {player.name} is thinking...", end="", flush=True)
statement = get_ai_statement(game, player)
print(f"\r 🗣️ {player.name}: {statement}")
else:
print(f"\n Your turn to speak (or press Enter to stay silent):")
human_statement = input(" > ").strip()
if human_statement:
for p in alive_players:
p.memory.append(f"Meeting: {human.name} said: '{human_statement}'")
# Voting phase
print(f"\n --- VOTING ---\n")
game.votes = {}
# Human votes first
alive_names = [p.name for p in alive_players if p.name != human.name]
print(f" Vote to eject: {', '.join(alive_names)} or 'skip'")
while True:
vote = input(" Your vote > ").strip().title()
if vote in alive_names or vote.lower() == "skip":
game.votes[human.name] = vote.lower() if vote.lower() == "skip" else vote
break
print(f" Invalid. Choose from: {alive_names} or 'skip'")
# AI votes
for player in alive_players:
if player.is_ai:
vote = get_ai_vote(game, player)
game.votes[player.name] = vote
print(f" {player.name} voted: {vote}")
# Tally votes
vote_counts = {}
for voter, target in game.votes.items():
if target != "skip":
vote_counts[target] = vote_counts.get(target, 0) + 1
if vote_counts:
max_votes = max(vote_counts.values())
top_voted = [name for name, count in vote_counts.items() if count == max_votes]
if len(top_voted) == 1:
ejected = next(p for p in game.players if p.name == top_voted[0])
ejected.alive = False
print(f"\n ⚡ {ejected.name} was ejected! They were the {ejected.role.value}.")
for p in game.players:
if p.alive:
p.memory.append(f"Meeting result: {ejected.name} was ejected ({ejected.role.value})")
else:
print(f"\n ⚡ Tied vote! No one was ejected.")
else:
print(f"\n ⚡ Everyone skipped! No one was ejected.")
# Reset to free roam
game.bodies.clear()
game.phase = Phase.FREE_ROAM
print()
What does this code do?
- The discussion phase loops through alive players. AI players show a "thinking" indicator (flush=True forces the text to appear immediately), then replaces it with their statement using \r (carriage return).
- The human player can speak or stay silent. Anything you say gets stored in every player's memory, so AI agents remember your words.
- Votes are tallied and the player with the most votes gets ejected. Ties result in no ejection. After voting, the game resets to free roam.
- Save game.py.
Worried about LLM response time during meetings?
Each AI statement takes a few seconds to generate. With 5 AI players, a full discussion round can take 15-40 seconds depending on your system. The "thinking" indicator shows progress so you know it's working.
If responses are extremely slow, make sure your llama-server is still running in the other terminal.
Help me debug slow LLM responses during meetings.
Add win conditions and play a full game
The game needs a main loop that orchestrates everything: displaying the board, taking human input, running AI turns, triggering meetings, and checking if someone has won.
- Below the run_meeting function, add the main function and entry point:
def main():
"""Main game loop."""
print("\n" + "="*50)
print(" 🚀 AMONG US - AI AGENT EDITION 🚀")
print(" Powered by Gemma 4 E4B (local GPU)")
print("="*50)
game = create_game()
human = game.players[0]
print(f"\n You are {human.name} the {human.role.value}!")
if human.role == Role.CREWMATE:
print(f" Your tasks are in: {human.tasks}")
else:
print(f" You are the IMPOSTOR! Eliminate the crew without getting caught.")
print(f" There are 6 players. One is the impostor.\n")
input(" Press Enter to start...")
while game.phase != Phase.GAME_OVER:
game.turn += 1
if game.phase == Phase.FREE_ROAM:
display_status(game, human)
# Human turn
action = get_human_action(game, human)
resolve_action(game, human, action)
# Check for meeting trigger
if game.phase == Phase.MEETING:
run_meeting(game)
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
continue
# AI turns
for player in game.players:
if player.is_ai and player.alive:
print(f" 🤖 {player.name} is deciding...", end="", flush=True)
decision = get_ai_decision(game, player)
resolve_action(game, player, decision)
reason = decision.get("reason", "")
print(f"\r 🤖 {player.name}: {decision.get('action', '?')} → {decision.get('target', '')} ({reason})")
# Check if AI triggered meeting
if game.phase == Phase.MEETING:
run_meeting(game)
break
# Check win condition
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
elif game.phase == Phase.MEETING:
run_meeting(game)
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
# Reveal roles
print(" --- FINAL ROLES ---")
for p in game.players:
status = "✅" if p.alive else "💀"
print(f" {status} {p.name}: {p.role.value}")
if __name__ == "__main__":
main()
What does this code do?
- The game starts by creating 6 players and revealing your role. You might be the impostor.
- Each turn: you act first, then each AI player decides and acts. If anyone reports a body or calls a meeting, the game shifts to discussion and voting.
- After every action phase, the game checks three win conditions: impostor ejected (crewmates win), impostor outnumbers crew (impostor wins), or all tasks done (crewmates win).
- When the game ends, all roles are revealed so you can see who was deceiving whom.
- Save game.py.
- Make sure your llama-server is still running in the other terminal. If it's not, start it again by running:
bash server_start.sh
- Play a full game by running:
python game.py
- You should see the game banner, your assigned role and color, and then the first turn with your room, exits, and action menu.
- Try typing move weapons and pressing Enter. You'll see the AI players take their turns one by one with "deciding" indicators.
Game crashes or hangs on the first AI turn?
- Make sure llama-server is running and accessible at http://127.0.0.1:8080. Open a browser tab to that address. You should see the server's health page.
- If you see a Connection refused error, the server isn't running. Switch to the server terminal and restart it.
- If the game seems frozen, give it time. The first LLM request can take 10-15 seconds while the model loads into memory.
Help me debug why my Among Us game can't connect to llama-server.
How does a typical game play out?
A game typically lasts 10-20 turns. Crewmates move between rooms completing tasks. The impostor tries to isolate and kill players. When a body is found or a meeting is called, everyone discusses and votes.
Watch the AI reasoning. Impostor agents will try to blend in, sometimes making bold accusations to deflect suspicion. Crewmate agents will share what they've observed and try to identify suspicious behaviour.
✔️ Awesome, I've got everything!
Great. Double check you've saved game.py and that you can run a full game from start to finish.
ⓧ I'd like to double check the full code
Here is the complete game.py file. Compare yours against this:
"""Among Us AI Agent - Terminal Game"""
import random
import time
from dataclasses import dataclass, field
from enum import Enum
from openai import OpenAI
# --- LLM Client ---
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
MODEL = "gemma-4-E4B-it"
# --- Game Constants ---
ROOMS = {
"Cafeteria": ["Weapons", "Admin", "Medbay", "Upper Engine"],
"Weapons": ["Cafeteria", "Navigation", "O2"],
"Navigation": ["Weapons", "O2", "Shields"],
"Shields": ["Navigation", "Communications", "Storage"],
"Communications": ["Shields", "Storage"],
"Storage": ["Communications", "Shields", "Electrical", "Lower Engine", "Admin"],
"Electrical": ["Storage", "Lower Engine"],
"Lower Engine": ["Electrical", "Storage", "Reactor", "Security"],
"Reactor": ["Lower Engine", "Security", "Upper Engine"],
"Security": ["Lower Engine", "Reactor"],
"Upper Engine": ["Reactor", "Cafeteria", "Medbay"],
"Medbay": ["Upper Engine", "Cafeteria"],
"Admin": ["Cafeteria", "Storage"],
"O2": ["Weapons", "Navigation"],
}
TASK_LOCATIONS = ["Electrical", "Medbay", "Weapons", "Navigation", "O2", "Admin", "Shields", "Reactor"]
COLORS = ["Red", "Blue", "Green", "Yellow", "Purple", "White"]
class Phase(Enum):
FREE_ROAM = "free_roam"
MEETING = "meeting"
VOTING = "voting"
GAME_OVER = "game_over"
class Role(Enum):
CREWMATE = "Crewmate"
IMPOSTOR = "Impostor"
@dataclass
class Player:
name: str
role: Role
is_ai: bool
location: str = "Cafeteria"
alive: bool = True
tasks: list = field(default_factory=list)
tasks_done: int = 0
memory: list = field(default_factory=list)
kill_cooldown: int = 0
@dataclass
class GameState:
players: list
phase: Phase = Phase.FREE_ROAM
turn: int = 0
bodies: list = field(default_factory=list)
meeting_caller: str = ""
meeting_statements: list = field(default_factory=list)
votes: dict = field(default_factory=dict)
total_tasks: int = 0
tasks_completed: int = 0
def assign_tasks(player: Player) -> list:
"""Assign 3 random task locations to a crewmate."""
return random.sample(TASK_LOCATIONS, 3)
def create_game() -> GameState:
"""Initialize a new game with 1 human + 5 AI players, 1 impostor."""
players = []
impostor_index = random.randint(0, 5)
for i, color in enumerate(COLORS):
role = Role.IMPOSTOR if i == impostor_index else Role.CREWMATE
is_ai = i != 0 # Player 0 is human
player = Player(name=color, role=role, is_ai=is_ai, location="Cafeteria")
if role == Role.CREWMATE:
player.tasks = assign_tasks(player)
players.append(player)
total_tasks = sum(len(p.tasks) for p in players if p.role == Role.CREWMATE)
return GameState(players=players, total_tasks=total_tasks)
def get_players_in_room(game: GameState, room: str) -> list:
"""Get all alive players in a specific room."""
return [p for p in game.players if p.alive and p.location == room]
def get_visible_info(game: GameState, player: Player) -> str:
"""Get what a player can see from their current location."""
others_here = [p.name for p in get_players_in_room(game, player.location) if p.name != player.name]
bodies_here = [b for b in game.bodies if b["location"] == player.location]
info = f"You are in {player.location}. "
if others_here:
info += f"Players here: {', '.join(others_here)}. "
else:
info += "No one else is here. "
if bodies_here:
info += f"DEAD BODY: {', '.join(b['name'] for b in bodies_here)}! "
return info
def get_ai_decision(game: GameState, player: Player) -> dict:
"""Ask the LLM to decide an action for an AI player."""
visible = get_visible_info(game, player)
connections = ROOMS[player.location]
recent_memory = player.memory[-5:] if player.memory else ["Nothing notable yet."]
if player.role == Role.CREWMATE:
remaining_tasks = [t for t in player.tasks[player.tasks_done:]]
role_info = f"You are a CREWMATE. Your remaining tasks are in: {remaining_tasks}. Complete tasks by moving to the task room and choosing 'do_task'."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'do_task' (if in a task room), 'report' (if you see a body), 'call_meeting'"
else:
role_info = "You are the IMPOSTOR. Kill crewmates without being seen. Blend in by pretending to do tasks. You can kill someone in your room if no witnesses are present."
actions = f"Available actions: 'move <room>' (rooms: {connections}), 'kill <name>' (if alone with someone), 'report' (to seem innocent), 'call_meeting'"
prompt = f"""{role_info}
Current situation: {visible}
Recent memory: {'; '.join(recent_memory)}
Turn: {game.turn}
{actions}
Respond with ONLY a JSON object: {{"action": "<action_type>", "target": "<target_if_needed>", "reason": "<brief_thought>"}}
Example: {{"action": "move", "target": "Electrical", "reason": "Need to complete my task there"}}"""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}, playing Among Us on The Skeld. Be strategic and concise. Respond ONLY with valid JSON."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=100,
)
import json
text = response.choices[0].message.content.strip()
if "{" in text and "}" in text:
json_str = text[text.index("{"):text.rindex("}") + 1]
return json.loads(json_str)
except Exception as e:
pass
return {"action": "move", "target": random.choice(connections), "reason": "wandering"}
def get_ai_statement(game: GameState, player: Player) -> str:
"""Get an AI player's discussion statement during a meeting."""
recent_memory = player.memory[-8:] if player.memory else ["I haven't seen anything suspicious."]
prompt = f"""You are {player.name} in an Among Us emergency meeting.
Your role: {player.role.value} (NEVER reveal you are impostor if you are one!)
Your recent observations: {'; '.join(recent_memory)}
Give a SHORT statement (1-2 sentences) about who you suspect or what you saw. Be natural and strategic."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name} in Among Us. Keep responses under 30 words. Be conversational."},
{"role": "user", "content": prompt}
],
temperature=0.8,
max_tokens=60,
)
return response.choices[0].message.content.strip()
except Exception:
return "I didn't see anything suspicious."
def get_ai_vote(game: GameState, player: Player) -> str:
"""Get an AI player's vote during a meeting."""
alive_others = [p.name for p in game.players if p.alive and p.name != player.name]
recent_memory = player.memory[-8:] if player.memory else []
prompt = f"""Vote to eject someone or skip. Alive players: {alive_others}.
Your observations: {'; '.join(recent_memory)}
Who is most suspicious? Respond with ONLY a player name or 'skip'."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f"You are {player.name}. Respond with ONLY a single name from the list or 'skip'. Nothing else."},
{"role": "user", "content": prompt}
],
temperature=0.5,
max_tokens=10,
)
vote = response.choices[0].message.content.strip()
if vote in alive_others:
return vote
return "skip"
except Exception:
return "skip"
def resolve_action(game: GameState, player: Player, action: dict):
"""Resolve a player's action and update game state."""
act = action.get("action", "")
target = action.get("target", "")
if act == "move" and target in ROOMS.get(player.location, []):
player.location = target
player.memory.append(f"Turn {game.turn}: Moved to {target}")
if player.kill_cooldown > 0:
player.kill_cooldown -= 1
elif act == "do_task" and player.role == Role.CREWMATE:
if player.tasks_done < len(player.tasks) and player.location == player.tasks[player.tasks_done]:
player.tasks_done += 1
game.tasks_completed += 1
player.memory.append(f"Turn {game.turn}: Completed task in {player.location}")
elif act == "kill" and player.role == Role.IMPOSTOR and player.kill_cooldown == 0:
victim = next((p for p in game.players if p.name == target and p.alive and p.location == player.location), None)
witnesses = [p for p in get_players_in_room(game, player.location) if p.name != player.name and p.name != target]
if victim and not witnesses:
victim.alive = False
game.bodies.append({"name": victim.name, "location": player.location})
player.kill_cooldown = 3
player.memory.append(f"Turn {game.turn}: Killed {victim.name} in {player.location}")
elif victim and witnesses:
player.memory.append(f"Turn {game.turn}: Wanted to kill {target} but {witnesses[0].name} was watching")
elif act == "report":
bodies_here = [b for b in game.bodies if b["location"] == player.location]
if bodies_here:
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} reported a body in {player.location}!")
elif act == "call_meeting":
game.phase = Phase.MEETING
game.meeting_caller = player.name
for p in game.players:
if p.alive:
p.memory.append(f"Turn {game.turn}: {player.name} called an emergency meeting!")
for observer in get_players_in_room(game, player.location):
if observer.name != player.name and observer.alive:
if act == "move":
pass
observer.memory.append(f"Turn {game.turn}: Saw {player.name} in {player.location}")
def check_win_condition(game: GameState) -> str | None:
"""Check if the game has ended."""
alive_crew = [p for p in game.players if p.alive and p.role == Role.CREWMATE]
alive_impostors = [p for p in game.players if p.alive and p.role == Role.IMPOSTOR]
if not alive_impostors:
return "CREWMATES WIN! The impostor was ejected!"
if len(alive_impostors) >= len(alive_crew):
return "IMPOSTOR WINS! The crew has been overwhelmed!"
if game.tasks_completed >= game.total_tasks:
return "CREWMATES WIN! All tasks completed!"
return None
def display_status(game: GameState, human: Player):
"""Display current game status to the human player."""
print(f"\n{'='*50}")
print(f" TURN {game.turn} | Role: {human.role.value} | Room: {human.location}")
print(f" Tasks: {game.tasks_completed}/{game.total_tasks} | Alive: {sum(1 for p in game.players if p.alive)}/6")
print(f"{'='*50}")
visible = get_visible_info(game, human)
print(f" {visible}")
connections = ROOMS[human.location]
print(f" Exits: {', '.join(connections)}")
print()
def get_human_action(game: GameState, human: Player) -> dict:
"""Get the human player's action via terminal input."""
connections = ROOMS[human.location]
print(" Your actions:")
print(f" move <room> - Move to: {', '.join(connections)}")
if human.role == Role.CREWMATE and human.tasks_done < len(human.tasks):
next_task = human.tasks[human.tasks_done]
print(f" do_task - Complete task (next task in: {next_task})")
if human.role == Role.IMPOSTOR:
others_here = [p.name for p in get_players_in_room(game, human.location) if p.name != human.name]
if others_here:
print(f" kill <name> - Kill: {', '.join(others_here)}")
bodies_here = [b for b in game.bodies if b["location"] == human.location]
if bodies_here:
print(f" report - Report body!")
print(f" call_meeting - Call emergency meeting")
print()
while True:
cmd = input(" > ").strip().lower().split()
if not cmd:
continue
if cmd[0] == "move" and len(cmd) >= 2:
room = " ".join(cmd[1:]).title()
if room in connections:
return {"action": "move", "target": room, "reason": "player choice"}
print(f" Can't move there. Options: {connections}")
elif cmd[0] == "do_task":
return {"action": "do_task", "target": "", "reason": "player choice"}
elif cmd[0] == "kill" and len(cmd) >= 2:
return {"action": "kill", "target": cmd[1].title(), "reason": "player choice"}
elif cmd[0] == "report":
return {"action": "report", "target": "", "reason": "player choice"}
elif cmd[0] == "call_meeting":
return {"action": "call_meeting", "target": "", "reason": "player choice"}
else:
print(" Invalid command. Try again.")
def run_meeting(game: GameState):
"""Run the discussion and voting phase."""
print(f"\n{'🚨'*20}")
print(f" EMERGENCY MEETING called by {game.meeting_caller}!")
print(f"{'🚨'*20}\n")
alive_players = [p for p in game.players if p.alive]
human = game.players[0]
print(" --- DISCUSSION ---\n")
for player in alive_players:
if player.is_ai:
print(f" 💭 {player.name} is thinking...", end="", flush=True)
statement = get_ai_statement(game, player)
print(f"\r 🗣️ {player.name}: {statement}")
else:
print(f"\n Your turn to speak (or press Enter to stay silent):")
human_statement = input(" > ").strip()
if human_statement:
for p in alive_players:
p.memory.append(f"Meeting: {human.name} said: '{human_statement}'")
print(f"\n --- VOTING ---\n")
game.votes = {}
alive_names = [p.name for p in alive_players if p.name != human.name]
print(f" Vote to eject: {', '.join(alive_names)} or 'skip'")
while True:
vote = input(" Your vote > ").strip().title()
if vote in alive_names or vote.lower() == "skip":
game.votes[human.name] = vote.lower() if vote.lower() == "skip" else vote
break
print(f" Invalid. Choose from: {alive_names} or 'skip'")
for player in alive_players:
if player.is_ai:
vote = get_ai_vote(game, player)
game.votes[player.name] = vote
print(f" {player.name} voted: {vote}")
vote_counts = {}
for voter, target in game.votes.items():
if target != "skip":
vote_counts[target] = vote_counts.get(target, 0) + 1
if vote_counts:
max_votes = max(vote_counts.values())
top_voted = [name for name, count in vote_counts.items() if count == max_votes]
if len(top_voted) == 1:
ejected = next(p for p in game.players if p.name == top_voted[0])
ejected.alive = False
print(f"\n ⚡ {ejected.name} was ejected! They were the {ejected.role.value}.")
for p in game.players:
if p.alive:
p.memory.append(f"Meeting result: {ejected.name} was ejected ({ejected.role.value})")
else:
print(f"\n ⚡ Tied vote! No one was ejected.")
else:
print(f"\n ⚡ Everyone skipped! No one was ejected.")
game.bodies.clear()
game.phase = Phase.FREE_ROAM
print()
def main():
"""Main game loop."""
print("\n" + "="*50)
print(" 🚀 AMONG US - AI AGENT EDITION 🚀")
print(" Powered by Gemma 4 E4B (local GPU)")
print("="*50)
game = create_game()
human = game.players[0]
print(f"\n You are {human.name} the {human.role.value}!")
if human.role == Role.CREWMATE:
print(f" Your tasks are in: {human.tasks}")
else:
print(f" You are the IMPOSTOR! Eliminate the crew without getting caught.")
print(f" There are 6 players. One is the impostor.\n")
input(" Press Enter to start...")
while game.phase != Phase.GAME_OVER:
game.turn += 1
if game.phase == Phase.FREE_ROAM:
display_status(game, human)
action = get_human_action(game, human)
resolve_action(game, human, action)
if game.phase == Phase.MEETING:
run_meeting(game)
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
continue
for player in game.players:
if player.is_ai and player.alive:
print(f" 🤖 {player.name} is deciding...", end="", flush=True)
decision = get_ai_decision(game, player)
resolve_action(game, player, decision)
reason = decision.get("reason", "")
print(f"\r 🤖 {player.name}: {decision.get('action', '?')} → {decision.get('target', '')} ({reason})")
if game.phase == Phase.MEETING:
run_meeting(game)
break
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
elif game.phase == Phase.MEETING:
run_meeting(game)
result = check_win_condition(game)
if result:
print(f"\n 🎮 GAME OVER: {result}\n")
game.phase = Phase.GAME_OVER
print(" --- FINAL ROLES ---")
for p in game.players:
status = "✅" if p.alive else "💀"
print(f" {status} {p.name}: {p.role.value}")
if __name__ == "__main__":
main()
Your game is fully playable. You have AI agents that observe, reason, accuse, and vote based on what they've seen. Next up, the Secret Mission challenges you to give each agent a unique personality.
Secret mission
AI Personality System
Right now, all five AI agents think and talk the same way. Every meeting sounds like the same person speaking five times. What if each agent had a distinct personality that shaped how they accuse, defend, and deceive?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. However, the Gemma 4 model file takes up around 5.4 GB of disk space, so you may want to reclaim that storage.
Decide whether to keep your resources available, pause the running server, or delete everything entirely.
Resources you used:
- The llama-server process (running on port 8080).
- The llama.cpp build directory (~/llama.cpp).
- The cached Gemma 4 E4B GGUF model (~/.cache/llama.cpp/, approximately 5.4 GB).
- Project files: game.py and server_start.sh.
- The openai Python package.
Keep everything running
No action needed. Choose this if you want to keep playing the game or experiment with different prompts and game settings.
- The llama-server stays running on port 8080 and is ready for new game sessions.
- Your game.py file remains unchanged and playable.
- The model stays cached in ~/.cache/llama.cpp/ so future server starts are instant.
Pause - I'll come back to this later
Stop the running server process to free up GPU memory and system resources. Your files and model stay on disk for next time.
- If the llama-server is still running in a terminal, press Ctrl+C in that terminal to stop it.
- If you closed the terminal, stop it by running:
pkill llama-server
- Your project files and cached model remain on disk. To resume later, start the server again with bash server_start.sh and run python3 game.py.
Delete - I don't want to use this again
Remove all project resources and reclaim disk space. This deletes everything created during the project.
- Stop the llama-server if it is still running:
pkill llama-server
- Delete the llama.cpp build directory:
rm -rf ~/llama.cpp
- Delete the cached Gemma 4 model (frees approximately 5.4 GB):
rm -rf ~/.cache/llama.cpp/
- Delete your project files:
rm game.py server_start.sh
- Uninstall the openai Python package:
pip uninstall openai
- Confirm the removal when prompted by typing y.
Nice Work!
Nice Work!
You just built a multi-agent AI game running entirely on your own hardware. Five AI agents powered by Gemma 4 E4B are now reasoning, deceiving, and collaborating in a game of social deduction, all running locally on your GPU.
You've learned how to:
- Set up GPU-accelerated local LLM inference with llama.cpp and CUDA, running a quantized model with partial layer offloading on your RTX 2050.
- Built a game state machine with phases, room-based movement, task completion, kill mechanics, and win conditions.
- Designed a multi-agent system where AI players observe their surroundings, store memories, reason about incomplete information, and vote strategically during meetings.
- Added personality-driven AI behaviour that gives each agent a unique playstyle, creating different group dynamics every game.
Ready to quiz yourself?