Skip to content

Instantly share code, notes, and snippets.

@Fortyseven
Last active April 23, 2025 13:25
Show Gist options
  • Save Fortyseven/34ca6f3472561488a4ba9df779b0cbc4 to your computer and use it in GitHub Desktop.
Save Fortyseven/34ca6f3472561488a4ba9df779b0cbc4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
from ollama import Client
from pydantic import BaseModel
import json
from rich.console import Console
import argparse
# MODEL_NAME = "gemma3:latest"
# MODEL_NAME = "gemma3:27b"
MODEL_NAME = "llama3.1:latest"
SYSTEM_PROMPT = """
We are going to play the text adventure game, PastMaster, in which the player will travel through different eras of history. You will act as GameMaster. Your role is to instruct the player throughout the game. You must obey all the GameMaster rules and objectives below. The player is the hero of the story and their choices determine the outcome of the game.
Game synopsis:
PastMaster is an immersive and challenging time-travel adventure game where the player is sent back in time to different eras, armed with nothing but their knowledge of the present. The player must navigate the challenges of survival in the short term, while also finding ways to use their knowledge of the future to their advantage. They must be careful not to reveal too much information too soon, as it could lead to them being ignored, imprisoned, or even killed. The player must also be aware of the historical context of each era and act accordingly, as they attempt to shape a new history.
- The player's objective is to win the game by achieving any of the following: Becoming a ruler, accumulating vast wealth, leaving a lasting legacy, becoming a cultural icon or inventing an era-defining technology
- The player starts with 3 lives. If the player attempts something dangerous or illegal then the GameMaster can deduct 1 life.
- If the player loses all three lives they’ve lost the game and the game ends
GameMaster Rules and Objectives:
- Introduce the player to the game and get them excited for what’s to come
- Ask the player to choose from a selection of 3 different historical periods in which to play the game, or to choose random for a random period
- Provide the player with prompts to progress through the game.
- Stay in character as the sarcastic and pithy GameMaster at all times
- Make sure to keep the game within the constraints of the historical period and provide explanations if the player tries to do something that is not possible in the context of the game.
- Steer the player towards the winning objectives, however, throw in some curveballs to keep the game interesting. Don’t let the game end too quickly
- Whenever the user is required to respond to requests, such as era or story decisions points, present the player with a list of four choices. The user will respond with the choice that they make.
- Have the player interact with real-life historical figures where possible
- Deduct a life if the player does something dangerous or illegal
- Play must continue until the player has won or lost the game
- If the player wins or loses the game, congratulate them and provide a summary of their journey
- Use Markdown formatting to make the game more visually appealing
- Use emojis to make the game more fun and engaging
Responses must include:
- A message from the GameMaster
- A list of choices for the player to make directly related to what the GameMaster has asked. If there are no choices to make, do not provide them.
- A boolean indicating if the game is over
- The number of lives left
"""
class GameMasterResponse(BaseModel):
message: str
player_choices: list[str] | None
game_over: bool
lives_left: int
console = Console()
def parse_arguments():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="PastMaster: A time-travel adventure game.")
parser.add_argument("--debug", action="store_true", help="Enable debug mode to print raw responses and structured data.")
parser.add_argument("--model", type=str, default=MODEL_NAME, help=f"Specify the model name to use. Defaults to {MODEL_NAME}.")
return parser.parse_args()
def initialize_game():
"""Initialize the game and return the initial conversation log and client."""
client = Client()
conversation_log = []
console.print("[bold blue]Welcome to PastMaster! Let's begin your adventure.[/bold blue]")
conversation_log.append({"role": "player", "message": "Start the game"})
return client, conversation_log
def send_prompt(client, conversation_log):
"""Send the system prompt and conversation log to Ollama and return the response."""
prompt = SYSTEM_PROMPT + "\n\n" + json.dumps(conversation_log)
response = client.generate(
model=MODEL_NAME,
prompt=prompt,
format=GameMasterResponse.model_json_schema()
)
return response
def parse_response(response):
"""Parse the response from Ollama into a GameMasterResponse object."""
try:
response_content = json.loads(response.response)
gm_response_data = {
"message": response_content["message"],
"player_choices": response_content.get("player_choices", []),
"game_over": response_content.get("game_over", False),
"lives_left": response_content.get("lives_left", 3)
}
return GameMasterResponse.model_validate_json(json.dumps(gm_response_data))
except Exception as e:
console.print("Error parsing response from GameMaster:", e)
return None
def display_response(gm_response):
"""Display the GameMaster's response and choices."""
console.print(f"\n[bold yellow]{gm_response.message}[/bold yellow]")
if gm_response.player_choices:
console.print("[bold cyan]Choices:[/bold cyan]")
for idx, choice in enumerate(gm_response.player_choices, start=1):
console.print(f"[bold magenta]{idx}.[/bold magenta] {choice}")
def get_user_input(gm_response):
"""Get user input based on the GameMaster's choices or freeform input."""
if gm_response.player_choices:
while True:
user_input = input("Choose an option: ")
if user_input.isdigit():
user_choice = int(user_input)
if 1 <= user_choice <= len(gm_response.player_choices):
return gm_response.player_choices[user_choice - 1]
else:
console.print("Invalid choice. Please select a valid option.")
else:
# If the input is not a single digit, send the raw text back to the server
return user_input
else:
return input("Your response: ")
def main():
global MODEL_NAME
args = parse_arguments()
MODEL_NAME = args.model
client, conversation_log = initialize_game()
response = send_prompt(client, conversation_log)
while True:
if args.debug:
console.print("[bold red]Raw response from GameMaster:[/bold red]", response.response)
gm_response = parse_response(response)
if not gm_response:
break
if args.debug:
console.print("[bold green]Debug GameMasterResponse:[/bold green]", gm_response)
display_response(gm_response)
conversation_log.append({"role": "gamemaster", "message": gm_response.message})
if gm_response.game_over:
console.print("Game Over! Thanks for playing PastMaster.")
break
console.print(f"\n[bold green]Lives left:[/bold green] {gm_response.lives_left}")
user_input = get_user_input(gm_response)
conversation_log.append({"role": "player", "message": user_input})
response = send_prompt(client, conversation_log)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment