first commit

This commit is contained in:
tcsenpai 2025-01-11 14:02:38 +01:00
commit 59d733d5ba
24 changed files with 2818 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
__pycache__
saves
.DS_Store
.venv
.env

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.11

63
README.md Normal file
View File

@ -0,0 +1,63 @@
# LLMRPG
A simple 2D RPG game using LLMs to control NPCs behavior.
![LLMRPG](./screenshot.png)
## Important note
This project is made for fun and learning purposes. It is not meant to be a finished product or a good example of how to use LLMs in a game. It is a proof of concept and a starting point for further development. It is created with heavy help from an LLM too, on purpose. This is to demonstrate the potential of LLMs to control complex systems and create interesting behaviors.
## Features
- 2D top-down view
- Procedural world generation with simple rules and objects
- NPCs with unique personalities and behaviors
- NPCs store memories and learn from them
- Weather system with different weather effects
- Time system based on real time
- Save and load game
## Installation
Copy the `env.example` file to `.env` and fill in the missing values if needed.
### Using pip / venv
#### Optional (create virtual environment)
```bash
python -m venv .venv
source .venv/bin/activate
```
#### Install dependencies
```bash
pip install -r requirements.txt
```
### Using uv
```bash
uv venv .venv
uv pip install -r requirements.txt
```
## Run the game
### Using python
```bash
python src/main.py
```
### Using uv
```bash
uv run python src/main.py
```
## Requirements
- An Ollama server running locally or in the location defined in the `.env` file.

2
env.example Normal file
View File

@ -0,0 +1,2 @@
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1:latest

14
pyproject.toml Normal file
View File

@ -0,0 +1,14 @@
[project]
name = "lmmrpg"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"noise>=1.2.2",
"numpy>=2.2.1",
"ollama>=0.4.5",
"pygame>=2.6.1",
"python-dotenv>=1.0.1",
"requests>=2.32.3",
]

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
noise
numpy
ollama
pygame
python-dotenv
requests

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

20
src/config/colors.py Normal file
View File

@ -0,0 +1,20 @@
COLORS = {
# Basic colors
"player": (0, 0, 255), # Blue
"npc": (255, 0, 0), # Red
# Nature
"tree": (0, 100, 0), # Dark green
"tall_grass": (34, 139, 34), # Forest green
"flower": (255, 192, 203), # Pink
"mushroom": (210, 180, 140), # Tan
"berry_bush": (139, 0, 0), # Dark red
"water": (0, 191, 255), # Deep sky blue
# Buildings and structures
"house": (139, 69, 19), # Saddle brown
"well": (128, 128, 128), # Gray
# Paths and ground
"path": (210, 180, 140), # Tan
"stone": (169, 169, 169), # Dark gray
# Decorative
"fence": (160, 82, 45), # Sienna
}

10
src/config/settings.py Normal file
View File

@ -0,0 +1,10 @@
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "mistral")
# Window settings
WINDOW_SIZE = (1200, 800)

65
src/effects/weather.py Normal file
View File

@ -0,0 +1,65 @@
import pygame
import random
class WeatherEffect:
def __init__(self, screen_width, screen_height):
self.width = screen_width
self.height = screen_height
self.rain_drops = []
self.cloud_overlay = pygame.Surface(
(screen_width, screen_height), pygame.SRCALPHA
)
self.sunny_overlay = pygame.Surface(
(screen_width, screen_height), pygame.SRCALPHA
)
# Initialize overlays
self.init_overlays()
def init_overlays(self):
# Cloudy overlay (gray)
pygame.draw.rect(
self.cloud_overlay, (100, 100, 100, 40), (0, 0, self.width, self.height)
)
# Sunny overlay (slight yellow tint)
pygame.draw.rect(
self.sunny_overlay, (255, 255, 200, 15), (0, 0, self.width, self.height)
)
def update_rain(self):
# Add new raindrops
if len(self.rain_drops) < 500:
self.rain_drops.append(
[
random.randint(0, self.width),
random.randint(-10, 0),
random.randint(4, 7), # Speed
]
)
# Update existing raindrops
for drop in self.rain_drops[:]:
drop[1] += drop[2] # Move down by speed
if drop[1] > self.height:
self.rain_drops.remove(drop)
def draw(self, screen, weather):
if weather == "rainy":
# Update and draw rain
self.update_rain()
for drop in self.rain_drops:
pygame.draw.line(
screen, (200, 200, 255), (drop[0], drop[1]), (drop[0], drop[1] + 5)
)
# Add rain overlay
screen.blit(self.cloud_overlay, (0, 0))
elif weather == "cloudy":
# Just add cloud overlay
screen.blit(self.cloud_overlay, (0, 0))
elif weather == "sunny":
# Add sunny overlay
screen.blit(self.sunny_overlay, (0, 0))

671
src/entities/npc.py Normal file
View File

@ -0,0 +1,671 @@
import random
import math
import pygame
import logging
from config.colors import COLORS
from datetime import datetime
import json
from enum import Enum
# Configure logger for NPC class
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# Create formatter
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
# Add the console handler to the logger
logger.addHandler(ch)
class MemoryType(Enum):
OBSERVATION = "observation"
INTERACTION = "interaction"
WEATHER = "weather"
TIME = "time"
SOCIAL = "social"
ACTIVITY = "activity"
class Memory:
def __init__(self, event, memory_type, importance=1.0):
self.event = event
self.timestamp = datetime.now().isoformat()
self.type = memory_type
self.importance = importance # 0.0 to 1.0
self.age = 0 # Will increase over time
class NPC:
# List of first names and surnames for more realistic naming
FIRST_NAMES = [
"Emma",
"Liam",
"Olivia",
"Noah",
"Ava",
"Oliver",
"Isabella",
"William",
"Sophia",
"James",
"Mia",
"Benjamin",
"Charlotte",
"Lucas",
"Amelia",
"Mason",
"Harper",
"Ethan",
"Evelyn",
"Alexander",
"Abigail",
"Henry",
"Emily",
"Sebastian",
"Elizabeth",
"Jack",
"Sofia",
"Owen",
"Avery",
"Daniel",
"Ella",
"Matthew",
"Scarlett",
"Joseph",
"Victoria",
]
SURNAMES = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
"Wilson",
"Anderson",
"Thomas",
"Taylor",
"Moore",
"Jackson",
"Martin",
"Lee",
"Perez",
"Thompson",
"White",
"Harris",
"Sanchez",
"Clark",
"Ramirez",
"Lewis",
"Robinson",
"Walker",
"Young",
"Allen",
"King",
]
def __init__(self, x, y, name):
self.x = x
self.y = y
self.name = name
self.radius = 15
self.speed = 1.0
self.interaction_radius = 50
self.memories = []
self.memory_limit = 50
self.last_weather_check = pygame.time.get_ticks()
self.weather_check_interval = 300
self.long_term_memories = {
"player_name": None,
"conversations": [],
"important_facts": {},
"relationships": {},
"daily_routine": [],
"preferences": self.generate_preferences(),
}
self.home_x = x
self.home_y = y
self.mood = random.choice(["happy", "neutral", "thoughtful", "busy"])
self.current_activity = None
self.color = self.generate_npc_color()
# Simple movement variables
self.move_angle = random.uniform(0, 2 * math.pi)
self.move_timer = 0
self.is_talking = False # Add this flag
self.original_pos = None # Store position when conversation starts
def generate_preferences(self):
"""Generate random preferences for the NPC"""
return {
"favorite_time": random.choice(
["morning", "afternoon", "evening", "night"]
),
"favorite_weather": random.choice(["sunny", "rainy", "cloudy"]),
"favorite_activity": random.choice(
["walking", "talking", "observing", "working"]
),
"personality": random.choice(["outgoing", "shy", "curious", "reserved"]),
}
def generate_npc_color(self):
"""Generate a pleasant, unique color for the NPC"""
# Base colors for villagers (warm, earthy tones)
base_colors = [
(139, 69, 19), # Saddle brown
(160, 82, 45), # Sienna
(205, 133, 63), # Peru
(210, 105, 30), # Chocolate
(184, 134, 11), # Dark goldenrod
(165, 42, 42), # Brown
(128, 70, 27), # Russet
(139, 90, 43), # Leather
(153, 101, 21), # Golden brown
(130, 102, 68), # Beaver
]
return random.choice(base_colors)
def update(self, world, npcs, player, current_time=None, weather=None):
"""Movement system with conversation handling"""
# If in conversation, stay still
if self.is_talking:
logger.debug(f"NPC {self.name} is in conversation, staying still")
return
# Debug current position and planned movement
#logger.debug(
# f"NPC {self.name} current position: ({int(self.x)}, {int(self.y)})"
#)
# Check if on or near a road/path
on_path = False
nearest_path = None
nearest_path_dist = float("inf")
for obj in world.objects:
if getattr(obj, "type", None) in ["road", "path"]:
dx = self.x - obj.x
dy = self.y - obj.y
dist = math.sqrt(dx * dx + dy * dy)
if dist < (self.radius + obj.radius):
on_path = True
self.speed = 1.5 # Slightly faster on paths
elif dist < nearest_path_dist:
nearest_path_dist = dist
nearest_path = obj
if not on_path:
self.speed = 1.0 # Normal speed off paths
# Try to move towards nearest path occasionally
if (
nearest_path and random.random() < 0.02
): # 2% chance to head towards path
angle = math.atan2(nearest_path.y - self.y, nearest_path.x - self.x)
self.move_angle = angle
#logger.debug(f"NPC {self.name} heading towards path")
# Check if NPC is stuck inside a house
for obj in world.objects:
# Skip roads for collision
if getattr(obj, "type", None) in ["road", "path"]:
continue
dx = self.x - obj.x
dy = self.y - obj.y
distance = math.sqrt(dx * dx + dy * dy)
if distance < (self.radius + obj.radius):
logger.debug(
f"NPC {self.name} is inside object {obj.__class__.__name__} at ({int(obj.x)}, {int(obj.y)})"
)
# Try to escape by moving away from object center
escape_angle = math.atan2(dy, dx)
self.x = obj.x + math.cos(escape_angle) * (obj.radius + self.radius + 5)
self.y = obj.y + math.sin(escape_angle) * (obj.radius + self.radius + 5)
logger.debug(
f"NPC {self.name} escaped to ({int(self.x)}, {int(self.y)})"
)
return
# Try to move in X direction first
move_x = math.cos(self.move_angle) * self.speed
new_x = self.x + move_x
new_x = max(self.radius, min(world.size - self.radius, new_x))
# Check X movement with more lenient collision
can_move_x = True
for obj in world.objects:
# Skip roads for collision
if getattr(obj, "type", None) in ["road", "path"]:
continue
dx = new_x - obj.x
dy = self.y - obj.y
distance = math.sqrt(dx * dx + dy * dy)
if distance < (self.radius + obj.radius + 2): # Small buffer
can_move_x = False
logger.debug(
f"X collision with {obj.__class__.__name__} at ({int(obj.x)}, {int(obj.y)})"
)
break
if can_move_x:
self.x = new_x
#logger.debug(f"NPC {self.name} moved X to {int(self.x)}")
# Then try Y movement separately
move_y = math.sin(self.move_angle) * self.speed
new_y = self.y + move_y
new_y = max(self.radius, min(world.size - self.radius, new_y))
# Check Y movement with more lenient collision
can_move_y = True
for obj in world.objects:
# Skip roads for collision
if getattr(obj, "type", None) in ["road", "path"]:
continue
dx = self.x - obj.x
dy = new_y - obj.y
distance = math.sqrt(dx * dx + dy * dy)
if distance < (self.radius + obj.radius + 2): # Small buffer
can_move_y = False
logger.debug(
f"Y collision with {obj.__class__.__name__} at ({int(obj.x)}, {int(obj.y)})"
)
break
if can_move_y:
self.y = new_y
logger.debug(f"NPC {self.name} moved Y to {int(self.y)}")
# If both movements failed, try to move away from nearby objects
if not (can_move_x or can_move_y):
# Find nearest object
nearest_obj = None
nearest_dist = float("inf")
for obj in world.objects:
dx = self.x - obj.x
dy = self.y - obj.y
dist = math.sqrt(dx * dx + dy * dy)
if dist < nearest_dist:
nearest_dist = dist
nearest_obj = obj
if nearest_obj:
# Move away from nearest object
dx = self.x - nearest_obj.x
dy = self.y - nearest_obj.y
angle = math.atan2(dy, dx)
self.move_angle = angle
logger.debug(
f"NPC {self.name} moving away from object at angle {angle}"
)
else:
# Random new direction if no nearby objects
self.move_angle = random.uniform(0, 2 * math.pi)
logger.debug(
f"NPC {self.name} changing to random direction {self.move_angle}"
)
# Update memories and observations
self.age_memories()
self.observe_environment(world, npcs, player, current_time, weather)
self.update_activity(current_time)
def age_memories(self):
"""Age memories and remove old ones"""
for memory in self.memories:
memory.age += 1
# Remove very old memories unless they're important
self.memories = [m for m in self.memories if m.age < 1000 or m.importance > 0.8]
def observe_environment(self, world, npcs, player, current_time, weather):
"""Generate diverse observations about the environment"""
# Time-based observations
if current_time:
time_of_day = self.get_time_of_day(current_time)
if random.random() < 0.1:
self.add_memory(
f"It's {time_of_day} now", MemoryType.TIME, importance=0.3
)
# Weather observations
if (
weather
and (self.last_weather_check + self.weather_check_interval)
<= pygame.time.get_ticks()
):
self.add_memory(
f"The weather is {weather}", MemoryType.WEATHER, importance=0.4
)
self.last_weather_check = pygame.time.get_ticks()
# Social observations
for npc in npcs:
if npc != self and self.distance_to(npc) < 100:
if random.random() < 0.05:
activity = random.choice(
[
"walking around",
"looking at the surroundings",
"heading somewhere",
"working",
]
)
self.add_memory(
f"Saw {npc.name} {activity}", MemoryType.SOCIAL, importance=0.6
)
# Environment observations
nearby_objects = self.get_nearby_objects(world)
if nearby_objects and random.random() < 0.1:
obj = random.choice(nearby_objects)
observation = self.generate_object_observation(obj)
self.add_memory(observation, MemoryType.OBSERVATION, importance=0.5)
def generate_object_observation(self, obj):
"""Generate varied observations about objects"""
if obj.type == "tree":
return random.choice(
[
"The trees are providing nice shade",
"The leaves are rustling in the wind",
"This tree looks particularly old",
]
)
elif obj.type == "flower":
return random.choice(
[
"The flowers are blooming beautifully",
"There's a pleasant floral scent in the air",
"These flowers add color to the village",
]
)
elif obj.type == "house":
return random.choice(
[
"That's a well-maintained house",
"Someone's home looks cozy",
"The house has an interesting design",
]
)
elif obj.type == "path":
return random.choice(
[
"The path is well-worn from use",
"This path connects different parts of the village",
"People often use this route",
]
)
return f"I noticed a {obj.type} nearby"
def add_memory(self, event, memory_type, importance=1.0):
"""Add a new memory with type and importance"""
memory = Memory(event, memory_type, importance)
self.memories.append(memory)
# Keep memories within limit, remove oldest and least important first
if len(self.memories) > self.memory_limit:
self.memories.sort(key=lambda m: m.importance * (1.0 / (1.0 + m.age)))
self.memories.pop(0)
def get_memory_context(self):
"""Get enhanced context from memories for conversation"""
context = []
# Add personality and preferences (safely)
personality = self.long_term_memories.get("preferences", {}).get(
"personality", "friendly"
)
favorite_activity = self.long_term_memories.get("preferences", {}).get(
"favorite_activity", "chatting"
)
context.append(f"You are {personality} and prefer {favorite_activity}")
# Add current mood and activity
context.append(
f"You are currently {self.mood} and {self.current_activity or 'not doing anything specific'}"
)
# Add player knowledge
if self.long_term_memories["player_name"]:
context.append(
f"You know the player as {self.long_term_memories['player_name']}"
)
# Add recent memories, weighted by type and importance
weighted_memories = sorted(
self.memories,
key=lambda m: m.importance * (1.0 / (1.0 + m.age)),
reverse=True,
)[
:5
] # Take top 5 memories
for memory in weighted_memories:
context.append(memory.event)
return " ".join(context)
def get_nearby_objects(self, world):
"""Get objects within observation range"""
nearby = []
for obj in world.objects:
if self.distance_to_pos(obj.x, obj.y) < 150: # Observation range
nearby.append(obj)
return nearby
def distance_to(self, other):
"""Calculate distance to another entity"""
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
def distance_to_pos(self, x, y):
"""Calculate distance to a position"""
dx = x - self.x
dy = y - self.y
return math.sqrt(dx * dx + dy * dy)
def get_time_of_day(self, current_time):
"""Convert time to period of day"""
hour = current_time.hour
if 5 <= hour < 12:
return "morning"
elif 12 <= hour < 17:
return "afternoon"
elif 17 <= hour < 21:
return "evening"
else:
return "night"
def update_activity(self, current_time):
"""Update NPC's current activity based on time"""
if not current_time:
return
hour = current_time.hour
if 6 <= hour < 10:
self.current_activity = "starting the day"
elif 10 <= hour < 12:
self.current_activity = "working in the morning"
elif 12 <= hour < 14:
self.current_activity = "having lunch"
elif 14 <= hour < 18:
self.current_activity = "working in the afternoon"
elif 18 <= hour < 21:
self.current_activity = "spending evening time"
else:
self.current_activity = "resting"
def remember_conversation(self, player_message, npc_response):
"""Store important conversation details"""
timestamp = datetime.now().isoformat()
self.long_term_memories["conversations"].append(
{
"timestamp": timestamp,
"player_said": player_message,
"npc_said": npc_response,
}
)
# Try to extract player name if mentioned
if "my name is" in player_message.lower():
try:
name = player_message.lower().split("my name is")[1].strip()
self.long_term_memories["player_name"] = name
self.long_term_memories["important_facts"]["player_name"] = name
except:
pass
def choose_next_action(self, world):
"""Choose what to do next based on time and location"""
logger.debug(f"NPC {self.name} choosing next action")
time_of_day = self.get_time_of_day(datetime.now())
current_weather = getattr(self, "current_weather", "sunny")
action = random.choice(
["wander", "visit_point_of_interest", "return_home", "wait"]
)
logger.debug(f"NPC {self.name} chose action: {action}")
if action == "wander":
angle = random.uniform(0, 2 * math.pi)
distance = random.uniform(50, self.wander_radius)
target_x = self.x + math.cos(angle) * distance
target_y = self.y + math.sin(angle) * distance
logger.debug(
f"NPC {self.name} wandering to ({int(target_x)}, {int(target_y)})"
)
self.set_target(target_x, target_y)
elif action == "visit_point_of_interest":
# Find nearby interesting points (buildings, wells, etc.)
points = self.find_points_of_interest(world)
if points:
point = random.choice(points)
self.set_target(point.x, point.y)
else:
self.state = "waiting"
self.wait_time = random.randint(60, 180)
elif action == "return_home":
self.set_target(self.home_x, self.home_y)
elif action == "wait":
self.state = "waiting"
self.wait_time = random.randint(60, 180)
def set_target(self, x, y):
"""Set a new movement target"""
self.target_x = max(0, min(x, 3000)) # Assume world size is 3000
self.target_y = max(0, min(y, 3000))
self.state = "walking"
self.state_timer = 0
def find_points_of_interest(self, world):
"""Find interesting points within observation range"""
points = []
for obj in world.objects:
if (
obj.type in ["house", "well", "tree"]
and self.distance_to_pos(obj.x, obj.y) < self.wander_radius
):
points.append(obj)
return points
def force_random_movement(self, world):
"""Force NPC to move in a random direction"""
angle = random.uniform(0, 2 * math.pi)
distance = 50 # Short distance for default movement
# Try 8 different directions if initial direction is blocked
for _ in range(8):
target_x = self.x + math.cos(angle) * distance
target_y = self.y + math.sin(angle) * distance
# Check if movement is possible
if not world.check_collision(target_x, target_y, self.radius):
self.target_x = target_x
self.target_y = target_y
self.state = "walking"
self.state_timer = 0
logger.debug(
f"NPC {self.name} forced movement to ({int(target_x)}, {int(target_y)})"
)
return
# Try next direction
angle += math.pi / 4
logger.debug(f"NPC {self.name} couldn't find valid movement direction")
def move_to_target(self, world):
"""Handle movement towards target"""
dx = self.target_x - self.x
dy = self.target_y - self.y
distance = math.sqrt(dx * dx + dy * dy)
logger.debug(f"NPC {self.name} walking to target. Distance: {distance:.2f}")
if distance < self.speed: # Reached target
self.x = self.target_x
self.y = self.target_y
self.state = "idle"
self.state_timer = 0
self.target_x = None
self.target_y = None
logger.debug(f"NPC {self.name} reached target")
else:
# Smooth movement towards target
move_x = (dx / distance) * self.speed
move_y = (dy / distance) * self.speed
# Check for collision before moving
new_x = self.x + move_x
new_y = self.y + move_y
if not world.check_collision(new_x, new_y, self.radius):
self.x = new_x
self.y = new_y
logger.debug(f"NPC {self.name} moved to ({int(self.x)}, {int(self.y)})")
else:
logger.debug(f"NPC {self.name} collision detected")
self.force_random_movement(world) # Try moving in a different direction
def debug_collision(self, world, x, y):
"""Debug helper to check what's causing collisions"""
# Check world bounds
if (
x < self.radius
or x > world.size - self.radius
or y < self.radius
or y > world.size - self.radius
):
logger.debug(f"World bounds collision at ({int(x)}, {int(y)})")
return True
# Check objects
for obj in world.objects:
dx = x - obj.x
dy = y - obj.y
distance = math.sqrt(dx * dx + dy * dy)
if distance < (self.radius + obj.radius):
logger.debug(f"Object collision with {obj} at ({int(x)}, {int(y)})")
return True
return False

51
src/entities/player.py Normal file
View File

@ -0,0 +1,51 @@
import pygame
import math
from config.colors import COLORS
class Player:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
self.color = COLORS["player"]
self.radius = 15
self.speed = 3.0
def update(self, keys, world):
"""Update player position based on keyboard input"""
new_x = self.x
new_y = self.y
# Calculate movement based on key presses
if keys[pygame.K_w] or keys[pygame.K_UP]:
new_y -= self.speed
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
new_y += self.speed
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
new_x -= self.speed
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
new_x += self.speed
# Normalize diagonal movement
if (
keys[pygame.K_w]
or keys[pygame.K_UP]
or keys[pygame.K_s]
or keys[pygame.K_DOWN]
) and (
keys[pygame.K_a]
or keys[pygame.K_LEFT]
or keys[pygame.K_d]
or keys[pygame.K_RIGHT]
):
new_x = self.x + (new_x - self.x) / math.sqrt(2)
new_y = self.y + (new_y - self.y) / math.sqrt(2)
# Check world boundaries
new_x = max(self.radius, min(world.size - self.radius, new_x))
new_y = max(self.radius, min(world.size - self.radius, new_y))
# Check collision with solid objects
if not world.check_collision(new_x, new_y, self.radius):
self.x = new_x
self.y = new_y

View File

@ -0,0 +1,45 @@
from config.colors import COLORS
class WorldObject:
def __init__(self, x: float, y: float, obj_type: str):
self.x = x
self.y = y
self.type = obj_type
self.color = COLORS[obj_type]
self.radius = 10 # Default radius
# Set attributes based on type
if obj_type == "house":
self.radius = 30
self.solid = True
self.render_layer = 2
elif obj_type == "well":
self.radius = 20
self.solid = True
self.render_layer = 2
elif obj_type == "tree":
self.radius = 15
self.solid = True
self.render_layer = 2
elif obj_type == "water":
self.radius = 10
self.solid = True
self.render_layer = 0
self.is_water = True
elif obj_type == "path":
self.radius = 10
self.solid = False
self.render_layer = 0
self.is_path = True
elif obj_type in ["flower", "mushroom", "tall_grass", "berry_bush"]:
self.radius = 5
self.solid = False
self.render_layer = 1
else:
self.solid = False
self.render_layer = 1
# Initialize all possible attributes to prevent AttributeError
self.is_water = getattr(self, "is_water", False)
self.is_path = getattr(self, "is_path", False)

508
src/main.py Normal file
View File

@ -0,0 +1,508 @@
import pygame
import asyncio
import math
import random
from ollama import Client
import logging
from datetime import datetime
from config.settings import WINDOW_SIZE, OLLAMA_URL, OLLAMA_MODEL
from config.colors import COLORS
from entities.npc import NPC
from utils.memory import Memory
from world.world import World
from world.camera import Camera
from entities.player import Player
from utils.dialog import DialogBox
from utils.save_manager import SaveManager
from menu.main_menu import MainMenu
from ui.header_bar import HeaderBar
from utils.name_generator import NameGenerator
from ui.footer_bar import FooterBar
from effects.weather import WeatherEffect
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode(WINDOW_SIZE)
pygame.display.set_caption("LMMRPG")
def draw_world_object(screen, obj, x, y):
"""Draw a world object with appropriate shape and color"""
# Ensure color is a valid RGB tuple
color = obj.color if isinstance(obj.color, tuple) else (0, 0, 0)
if obj.type == "path":
# Draw paths as rectangles
pygame.draw.rect(screen, color, (x - 10, y - 10, 20, 20))
elif obj.type == "water":
# Draw water as blue rectangles with slight transparency
surface = pygame.Surface((20, 20), pygame.SRCALPHA)
pygame.draw.rect(surface, (*color, 180), (0, 0, 20, 20)) # Add alpha channel
screen.blit(surface, (x - 10, y - 10))
elif obj.type == "house":
# Draw houses as brown rectangles with a triangular roof
pygame.draw.rect(
screen,
color,
(x - obj.radius, y - obj.radius, obj.radius * 2, obj.radius * 2),
)
# Roof
pygame.draw.polygon(
screen,
(139, 69, 19), # Darker brown for roof
[
(x - obj.radius - 5, y - obj.radius),
(x + obj.radius + 5, y - obj.radius),
(x, y - obj.radius - 15),
],
)
elif obj.type == "well":
# Draw wells as circles with a darker border
pygame.draw.circle(screen, color, (int(x), int(y)), obj.radius)
pygame.draw.circle(screen, (100, 100, 100), (int(x), int(y)), obj.radius, 2)
elif obj.type == "tree":
# Draw trees as green circles with brown trunks
# Trunk
pygame.draw.rect(screen, (139, 69, 19), (x - 3, y - 5, 6, 10)) # Brown
# Leaves
pygame.draw.circle(screen, color, (int(x), int(y - 10)), obj.radius)
elif obj.type in ["flower", "mushroom", "tall_grass", "berry_bush"]:
# Draw small decorative elements
if obj.type == "flower":
# Flowers as small colored circles with a center
pygame.draw.circle(screen, color, (int(x), int(y)), obj.radius)
pygame.draw.circle(
screen, (255, 255, 0), (int(x), int(y)), 2
) # Yellow center
elif obj.type == "mushroom":
# Mushrooms as small stems with caps
pygame.draw.rect(screen, (210, 180, 140), (x - 1, y - 3, 2, 6)) # Stem
pygame.draw.circle(screen, color, (int(x), int(y - 3)), 4) # Cap
elif obj.type == "tall_grass":
# Tall grass as small vertical lines
for i in range(-2, 3):
pygame.draw.line(
screen, color, (x + i, y), (x + i, y - random.randint(5, 8))
)
elif obj.type == "berry_bush":
# Berry bushes as green circles with red dots
pygame.draw.circle(
screen, (0, 100, 0), (int(x), int(y)), obj.radius
) # Bush
for _ in range(3): # Add berries
berry_x = x + random.randint(-3, 3)
berry_y = y + random.randint(-3, 3)
pygame.draw.circle(screen, (139, 0, 0), (int(berry_x), int(berry_y)), 2)
else:
# Default drawing for unknown objects
pygame.draw.circle(screen, color, (int(x), int(y)), obj.radius)
def draw_header(screen, world):
font = pygame.font.Font(None, 32)
header_height = 40
# Draw header background
pygame.draw.rect(screen, (0, 0, 0, 180), (0, 0, WINDOW_SIZE[0], header_height))
# Draw seed information
seed_text = f"World Seed: {world.seed}"
seed_surface = font.render(seed_text, True, (255, 255, 255))
screen.blit(seed_surface, (10, 10))
def draw_chat_input(screen, input_text, active):
# Draw chat input box at the very bottom
input_box_height = 40
input_box_width = WINDOW_SIZE[0] * 0.8
x = (WINDOW_SIZE[0] - input_box_width) / 2
y = WINDOW_SIZE[1] - input_box_height - 10 # Always at bottom
# Draw input box background
pygame.draw.rect(
screen,
(0, 0, 0, 180) if active else (50, 50, 50, 180),
(x, y, input_box_width, input_box_height),
)
# Draw input text
font = pygame.font.Font(None, 32)
if input_text:
text_surface = font.render(input_text, True, (255, 255, 255))
else:
text_surface = font.render(
"Type your message and press Enter...", True, (150, 150, 150)
)
screen.blit(text_surface, (x + 10, y + 10))
async def main():
pygame.init()
screen = pygame.display.set_mode(WINDOW_SIZE)
pygame.display.set_caption("LMMRPG")
save_manager = SaveManager()
menu = MainMenu(save_manager)
# Initialize save counter
save_counter = 0
save_cooldown = 300
# Start with menu
current_state = "menu"
world = None
player = None
camera = None
# Add time and weather system
current_time = datetime.now()
weather_conditions = ["sunny", "cloudy", "rainy"]
current_weather = random.choice(weather_conditions)
weather_change_counter = 0
header_bar = HeaderBar()
footer_bar = FooterBar()
weather_effect = WeatherEffect(WINDOW_SIZE[0], WINDOW_SIZE[1])
# Update world generation to use proper names
def generate_npcs(world):
for village_x, village_y in world.village_centers:
num_npcs = random.randint(3, 6)
for _ in range(num_npcs):
x = village_x + random.randint(-100, 100)
y = village_y + random.randint(-100, 100)
npc = NPC(x, y, NameGenerator.generate_name())
world.npcs.append(npc)
running = True
while running:
if current_state == "menu":
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
continue
result = menu.handle_input(event)
if result:
action, seed = result
if action == "quit":
running = False
elif action == "new":
world = World(seed=seed)
# Find valid spawn for player
spawn_x, spawn_y = world.find_valid_spawn(
world.size // 2, world.size // 2
)
player = Player(spawn_x, spawn_y)
camera = Camera()
current_state = "game"
elif action == "load":
save_data = save_manager.load_world(seed)
if save_data:
world_data, npc_data = save_data
world = World(seed=seed)
# Restore NPCs and their memories
for npc_info in npc_data:
for npc in world.npcs:
if npc.name == npc_info["name"]:
npc.memories = [
Memory(
m["event"],
m["timestamp"],
m["location"],
)
for m in npc_info["memories"]
]
npc.long_term_memories = npc_info.get(
"long_term_memories", []
)
player = Player(world.size // 2, world.size // 2)
camera = Camera()
current_state = "game"
menu.draw(screen)
pygame.display.flip()
continue
# Regular game loop
clock = pygame.time.Clock()
world = World()
player = Player(world.size // 2, world.size // 2)
camera = Camera()
ollama_client = Client(host=OLLAMA_URL)
dialog_box = DialogBox()
# Keep track of the last NPC we talked to
current_npc = None
conversation_history = []
input_text = ""
chat_active = False
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if chat_active:
if event.key == pygame.K_RETURN and input_text.strip():
# Handle chat input
dialog_box.show_message("You", input_text)
# Add to conversation history
conversation_history.append(
{"role": "user", "content": input_text}
)
# Generate NPC response
try:
# Get memory context from NPC
memory_context = current_npc.get_memory_context()
context = (
f"You are {current_npc.name}, a villager. "
f"{memory_context} "
f"Recent events: {[m.event for m in current_npc.memories[-3:]]}"
)
messages = [{"role": "system", "content": context}]
messages.extend(conversation_history[-6:])
response = ollama_client.chat(
model=OLLAMA_MODEL, messages=messages
)
# Store the conversation in NPC's memory
current_npc.remember_conversation(
input_text, response["message"]["content"]
)
# Add NPC response to history
conversation_history.append(
{
"role": "assistant",
"content": response["message"]["content"],
}
)
# Show NPC response
dialog_box.show_message(
current_npc.name, response["message"]["content"]
)
except Exception as e:
logger.error(f"Ollama error details: {str(e)}")
if hasattr(e, "response"):
logger.error(
f"Response status: {e.response.status_code}"
)
logger.error(f"Response content: {e.response.text}")
dialog_box.show_message(
current_npc.name,
f"I'm sorry, I didn't quite catch that... (Error: {str(e)})",
)
input_text = "" # Clear input
elif event.key == pygame.K_BACKSPACE:
input_text = input_text[:-1]
elif event.key == pygame.K_ESCAPE:
chat_active = False
dialog_box.hide()
current_npc = None
conversation_history = []
elif event.key == pygame.K_UP:
dialog_box.scroll_up()
elif event.key == pygame.K_DOWN:
dialog_box.scroll_down()
elif len(input_text) < 50 and event.unicode.isprintable():
input_text += event.unicode
else:
# Only handle non-chat keys when not chatting
if event.key == pygame.K_SPACE:
# Find nearby NPCs
nearby_npcs = []
for npc in world.npcs:
dist = math.sqrt(
(npc.x - player.x) ** 2 + (npc.y - player.y) ** 2
)
if dist <= npc.interaction_radius:
nearby_npcs.append(npc)
if nearby_npcs:
nearest_npc = min(
nearby_npcs,
key=lambda n: (
(n.x - player.x) ** 2 + (n.y - player.y) ** 2
),
)
if not chat_active:
# Start new conversation
current_npc = nearest_npc
chat_active = True
conversation_history = []
# Initial greeting
try:
context = f"You are {current_npc.name}, a villager. Your recent memories: {[m.event for m in current_npc.memories[-3:]]}"
response = ollama_client.chat(
model=OLLAMA_MODEL,
messages=[
{"role": "system", "content": context},
{"role": "user", "content": "Hello!"},
],
)
dialog_box.show_message(
current_npc.name,
response["message"]["content"],
)
conversation_history.append(
{
"role": "assistant",
"content": response["message"][
"content"
],
}
)
except Exception as e:
logger.error(f"Ollama error: {e}")
dialog_box.show_message(
current_npc.name,
"Hello traveler! (Ollama connection failed)",
)
else:
dialog_box.show_message(
"Info", "No NPCs nearby to talk to!"
)
# Only update player and world when not chatting
if not chat_active:
keys = pygame.key.get_pressed()
player.update(keys, world)
camera.update(player)
for npc in world.npcs:
npc.update(world, world.npcs, player)
# Drawing
screen.fill((34, 139, 34)) # Background grass
# Draw world objects by layer
for obj in world.objects:
screen_x = obj.x - camera.x
screen_y = obj.y - camera.y
if (
-50 <= screen_x <= WINDOW_SIZE[0] + 50
and -50 <= screen_y <= WINDOW_SIZE[1] + 50
):
draw_world_object(screen, obj, screen_x, screen_y)
# Draw NPCs
for npc in world.npcs:
screen_x = npc.x - camera.x
screen_y = npc.y - camera.y
if 0 <= screen_x <= WINDOW_SIZE[0] and 0 <= screen_y <= WINDOW_SIZE[1]:
# Draw interaction radius for nearby NPCs
dist_to_player = math.sqrt(
(npc.x - player.x) ** 2 + (npc.y - player.y) ** 2
)
if dist_to_player <= npc.interaction_radius:
pygame.draw.circle(
screen,
(255, 255, 255, 50),
(int(screen_x), int(screen_y)),
npc.interaction_radius,
1,
)
# Draw NPC
pygame.draw.circle(
screen, npc.color, (int(screen_x), int(screen_y)), npc.radius
)
pygame.draw.circle(
screen,
npc.color,
(int(screen_x), int(screen_y - npc.radius - 5)),
npc.radius - 5,
)
# Draw player
player_screen_x = WINDOW_SIZE[0] // 2
player_screen_y = WINDOW_SIZE[1] // 2
pygame.draw.circle(
screen, player.color, (player_screen_x, player_screen_y), player.radius
)
pygame.draw.circle(
screen,
player.color,
(player_screen_x, player_screen_y - player.radius - 5),
player.radius - 5,
)
# Draw weather effects last (on top of everything except UI)
weather_effect.draw(screen, current_weather)
# Draw UI elements after weather
header_bar.draw(
screen, current_time, current_weather, (player.x, player.y), world.size
)
footer_bar.draw(screen, world)
# Draw dialog box
dialog_box.draw(screen)
# Draw chat input if active
if chat_active:
draw_chat_input(screen, input_text, True)
pygame.display.flip()
clock.tick(60)
# Handle manual save on key press
keys = pygame.key.get_pressed()
if keys[pygame.K_F5]: # F5 to save
save_manager.save_world(world)
dialog_box.show_message("System", "Game saved!")
# Auto-save less frequently
save_counter += 1
if save_counter >= save_cooldown:
save_manager.save_world(world)
save_counter = 0
# Update NPC memories immediately after conversation
if chat_active and current_npc:
current_npc.remember_conversation(input_text, dialog_box.message)
save_manager.save_world(world) # Save immediately after memory update
# Update time every minute
if datetime.now().minute != current_time.minute:
current_time = datetime.now()
# Change weather occasionally
weather_change_counter += 1
if weather_change_counter >= 3600: # Change weather every ~1 minute
current_weather = random.choice(weather_conditions)
weather_change_counter = 0
# Update NPCs with time and weather
for npc in world.npcs:
npc.current_weather = current_weather # Set weather before update
npc.update(world, world.npcs, player, current_time, current_weather)
# Update world's weather
world.current_weather = current_weather
# When quitting
pygame.quit()
if __name__ == "__main__":
asyncio.run(main())

148
src/menu/main_menu.py Normal file
View File

@ -0,0 +1,148 @@
import pygame
import random
from config.settings import WINDOW_SIZE
from config.colors import COLORS
class MainMenu:
def __init__(self, save_manager):
self.save_manager = save_manager
self.font = pygame.font.Font(None, 48)
self.small_font = pygame.font.Font(None, 32)
self.selected = 0
self.input_seed = ""
self.state = "main" # main, load, enter_seed
self.saves = []
self.bg_color = (0, 0, 0)
self.text_color = (255, 255, 255)
self.highlight_color = (255, 255, 0)
def update_saves(self):
self.saves = self.save_manager.get_available_saves()
def draw(self, screen):
screen.fill(self.bg_color)
if self.state == "main":
options = [
"New Game (Random Seed)",
"New Game (Enter Seed)",
"Load Game",
"Quit",
]
for i, option in enumerate(options):
color = self.highlight_color if i == self.selected else self.text_color
text = self.font.render(option, True, color)
x = WINDOW_SIZE[0] // 2 - text.get_width() // 2
y = WINDOW_SIZE[1] // 2 - len(options) * 30 + i * 60
screen.blit(text, (x, y))
elif self.state == "load":
# Draw back option
back_text = self.font.render(
"Back",
True,
self.highlight_color if self.selected == -1 else self.text_color,
)
screen.blit(back_text, (20, 20))
if not self.saves:
text = self.font.render("No saves found", True, self.text_color)
screen.blit(
text,
(WINDOW_SIZE[0] // 2 - text.get_width() // 2, WINDOW_SIZE[1] // 2),
)
else:
for i, save in enumerate(self.saves):
color = (
self.highlight_color if i == self.selected else self.text_color
)
text = self.font.render(
f"World {save['seed']} - {save['timestamp'][:10]}", True, color
)
x = WINDOW_SIZE[0] // 2 - text.get_width() // 2
y = 100 + i * 60
screen.blit(text, (x, y))
elif self.state == "enter_seed":
prompt = self.font.render(
"Enter Seed (numbers only):", True, self.text_color
)
seed_text = self.font.render(
self.input_seed or "...", True, self.highlight_color
)
screen.blit(
prompt,
(
WINDOW_SIZE[0] // 2 - prompt.get_width() // 2,
WINDOW_SIZE[1] // 2 - 60,
),
)
screen.blit(
seed_text,
(WINDOW_SIZE[0] // 2 - seed_text.get_width() // 2, WINDOW_SIZE[1] // 2),
)
instruction = self.small_font.render(
"Press Enter to confirm, Escape to cancel", True, self.text_color
)
screen.blit(
instruction,
(
WINDOW_SIZE[0] // 2 - instruction.get_width() // 2,
WINDOW_SIZE[1] // 2 + 60,
),
)
def handle_input(self, event):
if self.state == "main":
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.selected = (self.selected - 1) % 4
elif event.key == pygame.K_DOWN:
self.selected = (self.selected + 1) % 4
elif event.key == pygame.K_RETURN:
if self.selected == 0: # New Game (Random)
return ("new", random.randint(1, 999999))
elif self.selected == 1: # New Game (Seed)
self.state = "enter_seed"
self.input_seed = ""
elif self.selected == 2: # Load Game
self.state = "load"
self.selected = 0 # Start with first save selected
self.update_saves()
elif self.selected == 3: # Quit
return ("quit", None)
elif self.state == "load":
if event.type == pygame.KEYDOWN:
max_index = len(self.saves)
if event.key == pygame.K_UP:
self.selected = (self.selected - 1) % (max_index + 1)
elif event.key == pygame.K_DOWN:
self.selected = (self.selected + 1) % (max_index + 1)
elif event.key == pygame.K_RETURN:
if self.selected == max_index: # Back option
self.state = "main"
self.selected = 0
elif self.saves: # Load selected save
return ("load", self.saves[self.selected]["seed"])
elif event.key == pygame.K_ESCAPE:
self.state = "main"
self.selected = 0
elif self.state == "enter_seed":
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and self.input_seed:
return ("new", int(self.input_seed))
elif event.key == pygame.K_ESCAPE:
self.state = "main"
self.selected = 1
elif event.key == pygame.K_BACKSPACE:
self.input_seed = self.input_seed[:-1]
elif event.unicode.isnumeric() and len(self.input_seed) < 6:
self.input_seed += event.unicode
return None

25
src/ui/footer_bar.py Normal file
View File

@ -0,0 +1,25 @@
import pygame
class FooterBar:
def __init__(self):
self.height = 30
self.font = pygame.font.Font(None, 24)
self.bg_color = (0, 0, 0, 180)
self.text_color = (255, 255, 255)
def draw(self, screen, world):
# Create semi-transparent surface
footer_surface = pygame.Surface(
(screen.get_width(), self.height), pygame.SRCALPHA
)
pygame.draw.rect(
footer_surface, self.bg_color, (0, 0, screen.get_width(), self.height)
)
# Create seed text
seed_text = self.font.render(f"World Seed: {world.seed}", True, self.text_color)
# Position at bottom of screen
screen.blit(footer_surface, (0, screen.get_height() - self.height))
screen.blit(seed_text, (20, screen.get_height() - self.height + 5))

57
src/ui/header_bar.py Normal file
View File

@ -0,0 +1,57 @@
import pygame
from datetime import datetime
class HeaderBar:
def __init__(self):
self.height = 30
self.font = pygame.font.Font(None, 24)
self.bg_color = (0, 0, 0, 180)
self.text_color = (255, 255, 255)
self.weather_colors = {
"sunny": (255, 223, 0),
"cloudy": (169, 169, 169),
"rainy": (0, 191, 255),
}
def draw(self, screen, current_time, weather, player_pos, world_size):
# Create semi-transparent surface
header_surface = pygame.Surface(
(screen.get_width(), self.height), pygame.SRCALPHA
)
pygame.draw.rect(
header_surface, self.bg_color, (0, 0, screen.get_width(), self.height)
)
# Format time
time_str = current_time.strftime("%H:%M")
# Format position as percentage of world size
pos_x = round((player_pos[0] / world_size) * 100)
pos_y = round((player_pos[1] / world_size) * 100)
# Create text elements
time_text = self.font.render(f"Time: {time_str}", True, self.text_color)
weather_text = self.font.render(f"Weather: ", True, self.text_color)
weather_value = self.font.render(
weather, True, self.weather_colors.get(weather, self.text_color)
)
position_text = self.font.render(
f"Position: {pos_x}%, {pos_y}%", True, self.text_color
)
# Draw elements
screen.blit(header_surface, (0, 0))
# Layout elements with padding
padding = 20
screen.blit(time_text, (padding, 5))
# Weather (text + value)
weather_x = time_text.get_width() + padding * 2
screen.blit(weather_text, (weather_x, 5))
screen.blit(weather_value, (weather_x + weather_text.get_width(), 5))
# Position on the right side
position_x = screen.get_width() - position_text.get_width() - padding
screen.blit(position_text, (position_x, 5))

92
src/utils/dialog.py Normal file
View File

@ -0,0 +1,92 @@
import pygame
from config.settings import WINDOW_SIZE
class DialogBox:
def __init__(self):
self.active = False
self.message = ""
self.speaker = ""
self.width = WINDOW_SIZE[0] * 0.8
self.min_height = 100 # Minimum height
self.max_height = 250 # Maximum height
self.x = (WINDOW_SIZE[0] - self.width) / 2
self.font = pygame.font.Font(None, 32)
self.small_font = pygame.font.Font(None, 24)
self.padding = 20
self.text_color = (255, 255, 255)
self.bg_color = (0, 0, 0, 180)
self.line_height = 30
def calculate_height(self, speaker_text, message_lines):
"""Calculate required height based on content"""
content_height = (
self.padding * 2 # Top and bottom padding
+ len(message_lines) * self.line_height # Message lines
+ 40 # Space for instructions
)
return min(max(self.min_height, content_height), self.max_height)
def show_message(self, speaker, message):
self.active = True
self.message = message
self.speaker = speaker
def hide(self):
self.active = False
def draw(self, screen):
if not self.active:
return
# Calculate speaker text dimensions
speaker_color = (255, 255, 0) if self.speaker == "You" else self.text_color
speaker_text = self.font.render(f"{self.speaker}:", True, speaker_color)
# Word wrap message
words = self.message.split()
lines = []
current_line = []
for word in words:
current_line.append(word)
text = " ".join(current_line)
if self.font.size(text)[0] > self.width - (
self.padding * 3 + speaker_text.get_width()
):
current_line.pop()
lines.append(" ".join(current_line))
current_line = [word]
lines.append(" ".join(current_line))
# Calculate required height
self.height = self.calculate_height(speaker_text, lines)
# Update y position based on height
self.y = WINDOW_SIZE[1] - self.height - 60 # Space for input box
# Create background
dialog_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
pygame.draw.rect(dialog_surface, self.bg_color, (0, 0, self.width, self.height))
screen.blit(dialog_surface, (self.x, self.y))
# Draw speaker name
screen.blit(speaker_text, (self.x + self.padding, self.y + self.padding))
# Draw all message lines
for i, line in enumerate(lines):
text = self.font.render(line, True, self.text_color)
screen.blit(
text,
(
self.x + self.padding * 2 + speaker_text.get_width(),
self.y + self.padding + i * self.line_height,
),
)
# Draw chat instructions at bottom of box
prompt_text = self.small_font.render(
"Type your message and press Enter, ESC to end conversation",
True,
(200, 200, 200),
)
screen.blit(prompt_text, (self.x + self.padding, self.y + self.height - 30))

9
src/utils/memory.py Normal file
View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import Tuple
@dataclass
class Memory:
event: str
timestamp: float
location: Tuple[float, float]

View File

@ -0,0 +1,53 @@
import random
class NameGenerator:
first_names = [
"Ada",
"Finn",
"Luna",
"Kai",
"Nova",
"Zara",
"Ash",
"Milo",
"Iris",
"Theo",
"Sage",
"Remy",
"Jade",
"Leo",
"Aria",
"Owen",
"Ivy",
"Cole",
"Maya",
"Axel",
]
last_names = [
"Thorne",
"Frost",
"Rivers",
"Storm",
"Woods",
"Drake",
"Stone",
"Vale",
"Brook",
"Field",
"Grove",
"Swift",
"Dale",
"Marsh",
"Hill",
"Lake",
"Reed",
"Glen",
"Shaw",
"Ford",
]
@classmethod
def generate_name(cls):
return f"{random.choice(cls.first_names)} {random.choice(cls.last_names)}"

114
src/utils/save_manager.py Normal file
View File

@ -0,0 +1,114 @@
import os
import json
import logging
from datetime import datetime
# Configure logger for save manager
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING) # Only show warnings and errors
class SaveManager:
def __init__(self):
self.save_dir = "saves"
self.ensure_save_directory()
def ensure_save_directory(self):
"""Ensure save directory exists"""
if not os.path.exists(self.save_dir):
os.makedirs(self.save_dir)
def get_world_path(self, seed):
"""Get path for a specific world save"""
return os.path.join(self.save_dir, f"world_{seed}")
def save_world(self, world):
"""Save world state including NPCs and their memories"""
try:
world_dir = self.get_world_path(world.seed)
if not os.path.exists(world_dir):
os.makedirs(world_dir)
# Save world data
world_data = {
"seed": world.seed,
"size": world.size,
"timestamp": datetime.now().isoformat(),
"village_centers": world.village_centers,
}
with open(os.path.join(world_dir, "world.json"), "w") as f:
json.dump(world_data, f)
# Save NPCs and their memories
npc_data = []
for npc in world.npcs:
npc_info = {
"name": npc.name,
"x": npc.x,
"y": npc.y,
"home_x": npc.home_x,
"home_y": npc.home_y,
"memories": [
{
"event": m.event,
"timestamp": m.timestamp,
"type": m.type.value,
"importance": m.importance,
"age": m.age,
}
for m in npc.memories
],
"long_term_memories": npc.long_term_memories,
}
npc_data.append(npc_info)
with open(os.path.join(world_dir, "npcs.json"), "w") as f:
json.dump(npc_data, f)
logger.debug(f"Saved world with seed {world.seed}")
except Exception as e:
logger.error(f"Failed to save world: {e}")
def load_world(self, seed):
"""Load world state including NPCs and their memories"""
world_dir = self.get_world_path(seed)
if not os.path.exists(world_dir):
logger.error(f"No save found for seed {seed}")
return None
try:
# Load world data
with open(os.path.join(world_dir, "world.json"), "r") as f:
world_data = json.load(f)
# Load NPCs data
with open(os.path.join(world_dir, "npcs.json"), "r") as f:
npc_data = json.load(f)
return world_data, npc_data
except Exception as e:
logger.error(f"Error loading save for seed {seed}: {e}")
return None
def get_available_saves(self):
"""Get list of available saves"""
saves = []
for item in os.listdir(self.save_dir):
if item.startswith("world_"):
try:
world_path = os.path.join(self.save_dir, item, "world.json")
if os.path.exists(world_path):
with open(world_path, "r") as f:
data = json.load(f)
saves.append(
{
"seed": data["seed"],
"timestamp": data["timestamp"],
"path": item,
}
)
except Exception as e:
logger.error(f"Error reading save {item}: {e}")
return saves

26
src/world/camera.py Normal file
View File

@ -0,0 +1,26 @@
from config.settings import WINDOW_SIZE
class Camera:
def __init__(self):
self.x = 0
self.y = 0
self.smoothness = 0.1 # Lower = smoother camera
def update(self, target):
"""Smoothly follow the target (usually the player)"""
# Calculate desired position (centered on target)
desired_x = target.x - WINDOW_SIZE[0] // 2
desired_y = target.y - WINDOW_SIZE[1] // 2
# Smooth movement
self.x += (desired_x - self.x) * self.smoothness
self.y += (desired_y - self.y) * self.smoothness
def apply(self, x, y):
"""Convert world coordinates to screen coordinates"""
return (int(x - self.x), int(y - self.y))
def reverse_apply(self, screen_x, screen_y):
"""Convert screen coordinates to world coordinates"""
return (screen_x + self.x, screen_y + self.y)

467
src/world/world.py Normal file
View File

@ -0,0 +1,467 @@
import random
import noise
import math
from entities.world_object import WorldObject
from entities.npc import NPC
import logging
from utils.name_generator import NameGenerator
logger = logging.getLogger(__name__)
class World:
def __init__(self, seed=None):
logger.info(f"Initializing world with seed {seed}")
self.size = 3000
self.objects = []
self.npcs = []
self.seed = seed if seed is not None else random.randint(1, 99999)
self.village_centers = []
self.min_house_distance = 120
self.current_weather = "sunny" # Default weather
random.seed(self.seed)
self.generate_world()
def check_collision(
self, x: float, y: float, radius: float, ignore_entity=None
) -> bool:
"""Check if a position would result in a collision"""
# Check world boundaries
if (
x - radius < 0
or x + radius > self.size
or y - radius < 0
or y + radius > self.size
):
return True
# Check collision with solid objects
for obj in self.objects:
if not getattr(obj, "solid", False): # Skip non-solid objects
continue
distance = math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
min_distance = radius + obj.radius
if distance < min_distance:
return True
# Check collision with NPCs (if not checking for an NPC itself)
if ignore_entity is None or not isinstance(ignore_entity, NPC):
for npc in self.npcs:
if npc == ignore_entity:
continue
distance = math.sqrt((x - npc.x) ** 2 + (y - npc.y) ** 2)
min_distance = radius + npc.radius
if distance < min_distance:
return True
return False
def is_position_valid(
self, x: float, y: float, radius: float, min_distance: float = None
) -> bool:
"""Check if a position is valid for object placement"""
# Check world boundaries
if (
x - radius < 0
or x + radius > self.size
or y - radius < 0
or y + radius > self.size
):
return False
# Check distance from other objects
for obj in self.objects:
if getattr(obj, "solid", False): # Only check solid objects
dist = math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
min_dist = min_distance if min_distance else (radius + obj.radius)
if dist < min_dist:
return False
return True
def generate_village(self, center_x, center_y, size):
"""Generate a village with better spacing and organization"""
houses = []
# Create central plaza with well
well = WorldObject(center_x, center_y, "well")
well.render_layer = 2
well.solid = True
self.objects.append(well)
# Generate houses in a circular pattern with better spacing
num_houses = random.randint(4, 6) # Fewer houses for better spacing
base_distance = 150 # Increased base distance from center
for i in range(num_houses):
angle = (i / num_houses) * 2 * math.pi
distance = random.randint(base_distance, base_distance + 60)
attempts = 0
while attempts < 10:
house_x = center_x + math.cos(angle) * distance
house_y = center_y + math.sin(angle) * distance
# Add slight randomness to position
house_x += random.randint(-20, 20)
house_y += random.randint(-20, 20)
if self.is_position_valid(
house_x, house_y, 30, self.min_house_distance
):
house = WorldObject(house_x, house_y, "house")
house.solid = True
house.render_layer = 2 # Top layer
houses.append(house)
self.objects.append(house)
# Create paths from house to well
path_tiles = self.create_path(
(house_x, house_y), (center_x, center_y)
)
self.objects.extend(path_tiles)
# Add decorative elements around house
self.add_house_decorations(house_x, house_y)
# Add NPC
self.add_house_npc(house_x, house_y)
break
attempts += 1
return houses
def add_house_decorations(self, house_x, house_y):
"""Add decorative elements around a house"""
for _ in range(3):
attempts = 0
while attempts < 5:
dx = random.randint(-25, 25)
dy = random.randint(-25, 25)
dec_x = house_x + dx
dec_y = house_y + dy
if self.is_position_valid(dec_x, dec_y, 5):
if random.random() < 0.5:
self.objects.append(WorldObject(dec_x, dec_y, "flower"))
else:
self.objects.append(WorldObject(dec_x, dec_y, "berry_bush"))
break
attempts += 1
def add_house_npc(self, house_x, house_y):
"""Add an NPC near a house in a valid position"""
# Always try to place NPC on the path in front of the house
attempts = 0
while attempts < 30: # More attempts
# Try to place NPC on the path side of the house
angle = random.uniform(0, 2 * math.pi)
distance = random.randint(40, 60) # Much larger distance from house
npc_x = house_x + math.cos(angle) * distance
npc_y = house_y + math.sin(angle) * distance
# More thorough position validation
valid_position = (
0 <= npc_x <= self.size
and 0 <= npc_y <= self.size
and
# Check for path nearby (NPCs prefer to stand near paths)
any(
math.sqrt((obj.x - npc_x) ** 2 + (obj.y - npc_y) ** 2) < 30
for obj in self.objects
if getattr(obj, "is_path", False)
)
and
# Ensure no collision with solid objects
not any(
math.sqrt((obj.x - npc_x) ** 2 + (obj.y - npc_y) ** 2) < 30
for obj in self.objects
if getattr(obj, "solid", False)
)
)
if valid_position:
npc = NPC(npc_x, npc_y, f"Villager_{len(self.npcs)}")
npc.home_x = house_x
npc.home_y = house_y
self.npcs.append(npc)
logger.info(f"Created NPC {npc.name} at ({npc_x:.2f}, {npc_y:.2f})")
return True
attempts += 1
logger.warning(
f"Failed to place NPC for house at ({house_x:.2f}, {house_y:.2f})"
)
return False
def create_water_body(self, x, y, size):
"""Create a water body using tiles"""
water_tiles = []
tile_size = 20
for i in range(-size, size + 1):
for j in range(-size, size + 1):
# Create circular-ish water body
if (i * i + j * j) <= size * size:
water_x = x + i * tile_size
water_y = y + j * tile_size
if self.is_position_valid(water_x, water_y, tile_size / 2):
water = WorldObject(water_x, water_y, "water")
water.is_water = True
water_tiles.append(water)
return water_tiles
def create_river(self, start_x, start_y, length, width=30):
"""Generate a meandering river using rectangular tiles"""
river_tiles = []
current_x, current_y = start_x, start_y
angle = random.uniform(0, 2 * math.pi)
tile_size = 20 # Size of each water tile
for _ in range(length):
# Meander the river
angle += random.uniform(-0.2, 0.2)
# Create main river tiles (rectangular pattern)
for w in range(-width // 2, width // 2, tile_size):
perpendicular = angle + math.pi / 2
offset_x = math.cos(perpendicular) * w
offset_y = math.sin(perpendicular) * w
water = WorldObject(current_x + offset_x, current_y + offset_y, "water")
water.render_layer = 0
water.is_water = True
water.solid = True
water.width = tile_size # Add width for rectangular rendering
water.height = tile_size # Add height for rectangular rendering
river_tiles.append(water)
# Move to next position
current_x += math.cos(angle) * (
tile_size / 2
) # Overlap slightly for continuity
current_y += math.sin(angle) * (tile_size / 2)
# Add riverbank decoration
if random.random() < 0.1:
side = random.choice([-1, 1])
bank_x = current_x + math.cos(angle + math.pi / 2) * width * side
bank_y = current_y + math.sin(angle + math.pi / 2) * width * side
decor = WorldObject(
bank_x, bank_y, random.choice(["tall_grass", "flower"])
)
decor.render_layer = 1
river_tiles.append(decor)
return river_tiles
def create_forest(self, center_x, center_y, radius):
"""Generate a forest area"""
forest_objects = []
num_trees = int(radius * radius / 1000) # Scale number of trees with area
for _ in range(num_trees):
angle = random.uniform(0, 2 * math.pi)
distance = random.uniform(0, radius)
tree_x = center_x + math.cos(angle) * distance
tree_y = center_y + math.sin(angle) * distance
if self.is_position_valid(tree_x, tree_y, 15):
tree = WorldObject(tree_x, tree_y, "tree")
tree.render_layer = 2
tree.solid = True
forest_objects.append(tree)
# Add forest floor decoration
if random.random() < 0.3:
decor_x = tree_x + random.uniform(-20, 20)
decor_y = tree_y + random.uniform(-20, 20)
decor = WorldObject(
decor_x,
decor_y,
random.choice(["mushroom", "tall_grass", "flower"]),
)
decor.render_layer = 1
forest_objects.append(decor)
return forest_objects
def generate_world(self):
self.objects = []
self.npcs = []
self.village_centers = []
# Generate rivers first
num_rivers = random.randint(2, 4)
for _ in range(num_rivers):
start_x = random.randint(0, self.size)
start_y = random.randint(0, self.size)
river_tiles = self.create_river(
start_x, start_y, length=random.randint(30, 50)
)
self.objects.extend(river_tiles)
# Generate forests
num_forests = random.randint(4, 8)
for _ in range(num_forests):
forest_x = random.randint(200, self.size - 200)
forest_y = random.randint(200, self.size - 200)
# Check if position is valid (not in water)
if not any(
obj.is_water
for obj in self.objects
if math.sqrt((obj.x - forest_x) ** 2 + (obj.y - forest_y) ** 2) < 100
):
forest = self.create_forest(
forest_x, forest_y, random.randint(150, 300)
)
self.objects.extend(forest)
# Generate villages with proper spacing
num_villages = random.randint(3, 5)
min_village_distance = 400
for i in range(num_villages):
attempts = 0
while attempts < 50:
center_x = random.randint(300, self.size - 300)
center_y = random.randint(300, self.size - 300)
# Check if position is valid (not in water or forest)
if (
not any(
obj.is_water
for obj in self.objects
if math.sqrt((obj.x - center_x) ** 2 + (obj.y - center_y) ** 2)
< 200
)
and not any(
obj.type == "tree"
for obj in self.objects
if math.sqrt((obj.x - center_x) ** 2 + (obj.y - center_y) ** 2)
< 150
)
and (
not self.village_centers
or all(
math.sqrt((x - center_x) ** 2 + (y - center_y) ** 2)
> min_village_distance
for x, y in self.village_centers
)
)
):
self.village_centers.append((center_x, center_y))
break
attempts += 1
# Generate paths between villages (avoiding water)
for i in range(len(self.village_centers)):
for j in range(i + 1, len(self.village_centers)):
path_tiles = self.create_path(
self.village_centers[i], self.village_centers[j]
)
self.objects.extend(path_tiles)
# Generate villages
for center_x, center_y in self.village_centers:
self.generate_village(center_x, center_y, random.randint(80, 120))
# Sort objects by render layer
self.objects.sort(key=lambda obj: getattr(obj, "render_layer", 1))
def create_path(self, start, end):
"""Create a path between two points using rectangular tiles"""
path_tiles = []
dx = end[0] - start[0]
dy = end[1] - start[1]
distance = math.sqrt(dx * dx + dy * dy)
tile_size = 20
steps = int(distance / (tile_size / 2)) # Overlap tiles slightly for continuity
# Add some natural curve to the path
curve_strength = random.uniform(0.1, 0.3)
mid_point = (
(start[0] + end[0]) / 2 + random.randint(-100, 100) * curve_strength,
(start[1] + end[1]) / 2 + random.randint(-100, 100) * curve_strength,
)
# Create main path tiles
for i in range(steps + 1):
t = i / steps
# Quadratic Bezier curve
x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * mid_point[0] + t**2 * end[0]
y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * mid_point[1] + t**2 * end[1]
path = WorldObject(x, y, "path")
path.render_layer = 0 # Bottom layer
path.is_path = True
path.solid = False # Paths are walkable
path_tiles.append(path)
# Add path borders (decorative)
if random.random() < 0.05: # Reduced frequency of decorations
offset = random.randint(-15, 15)
if random.random() < 0.5:
flower = WorldObject(x + offset, y + offset, "flower")
flower.render_layer = 1
path_tiles.append(flower)
return path_tiles
def create_village_paths(self, houses, well):
"""Create paths connecting houses to the central well"""
for house in houses:
path_tiles = self.create_path((house.x, house.y), (well.x, well.y))
self.objects.extend(path_tiles)
def find_valid_spawn(self, x, y, radius=15, max_attempts=100):
"""Find a nearby valid spawn point that doesn't collide with solids"""
original_x, original_y = x, y
attempt = 0
while attempt < max_attempts:
# Try the current position
if not self.check_collision(x, y, radius):
return x, y
# Spiral out from original position
angle = 2 * math.pi * attempt / max_attempts
distance = (attempt / max_attempts) * 200 # Max 200 pixels from original
x = original_x + math.cos(angle) * distance
y = original_y + math.sin(angle) * distance
attempt += 1
# If no valid position found, try a completely random position
for _ in range(max_attempts):
x = random.randint(radius, self.size - radius)
y = random.randint(radius, self.size - radius)
if not self.check_collision(x, y, radius):
return x, y
logger.warning("Could not find valid spawn point!")
return original_x, original_y # Return original as last resort
def generate_npcs(self):
"""Generate NPCs in valid positions near village centers"""
for village_x, village_y in self.village_centers:
num_npcs = random.randint(3, 6)
for _ in range(num_npcs):
base_x = village_x + random.randint(-100, 100)
base_y = village_y + random.randint(-100, 100)
# Find valid spawn point near the intended position
x, y = self.find_valid_spawn(base_x, base_y)
npc = NPC(x, y, NameGenerator.generate_name())
self.npcs.append(npc)

366
uv.lock generated Normal file
View File

@ -0,0 +1,366 @@
version = 1
requires-python = ">=3.11"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "certifi"
version = "2024.12.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
{ url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
{ url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
{ url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
{ url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
{ url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
{ url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
{ url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
{ url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
{ url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
{ url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
{ url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
{ url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "lmmrpg"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "noise" },
{ name = "numpy" },
{ name = "ollama" },
{ name = "pygame" },
{ name = "python-dotenv" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "noise", specifier = ">=1.2.2" },
{ name = "numpy", specifier = ">=2.2.1" },
{ name = "ollama", specifier = ">=0.4.5" },
{ name = "pygame", specifier = ">=2.6.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "requests", specifier = ">=2.32.3" },
]
[[package]]
name = "noise"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/29/bb830ee6d934311e17a7a4fa1368faf3e73fbb09c0d80fc44e41828df177/noise-1.2.2.tar.gz", hash = "sha256:57a2797436574391ff63a111e852e53a4164ecd81ad23639641743cd1a209b65", size = 125615 }
[[package]]
name = "numpy"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379 },
{ url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520 },
{ url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286 },
{ url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345 },
{ url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748 },
{ url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057 },
{ url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943 },
{ url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785 },
{ url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983 },
{ url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260 },
{ url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861 },
{ url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776 },
{ url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239 },
{ url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296 },
{ url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121 },
{ url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599 },
{ url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932 },
{ url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032 },
{ url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018 },
{ url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843 },
{ url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464 },
{ url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350 },
{ url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629 },
{ url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865 },
{ url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508 },
{ url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100 },
{ url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691 },
{ url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571 },
{ url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841 },
{ url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618 },
{ url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004 },
{ url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910 },
{ url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612 },
{ url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401 },
{ url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198 },
{ url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211 },
{ url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266 },
{ url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844 },
{ url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007 },
{ url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107 },
]
[[package]]
name = "ollama"
version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/fd/a130173a62fd6dc7f7875919593b1e7a47bf3870a913c35d51ea013cfe41/ollama-0.4.5.tar.gz", hash = "sha256:e7fb71a99147046d028ab8b75e51e09437099aea6f8f9a0d91a71f787e97439e", size = 13104 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/71/44e508b6be7cc12efc498217bf74f443dbc1a31b145c87421d20fe61b70b/ollama-0.4.5-py3-none-any.whl", hash = "sha256:74936de89a41c87c9745f09f2e1db964b4783002188ac21241bfab747f46d925", size = 13205 },
]
[[package]]
name = "pydantic"
version = "2.10.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pygame"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753 },
{ url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146 },
{ url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760 },
{ url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054 },
{ url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107 },
{ url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863 },
{ url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016 },
{ url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279 },
{ url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524 },
{ url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123 },
{ url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532 },
{ url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653 },
{ url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421 },
{ url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591 },
{ url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765 },
{ url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704 },
{ url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091 },
{ url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844 },
{ url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197 },
{ url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309 },
{ url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]