mirror of
https://github.com/tcsenpai/llm_world.git
synced 2025-06-02 17:20:05 +00:00
first commit
This commit is contained in:
commit
59d733d5ba
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
__pycache__
|
||||
saves
|
||||
.DS_Store
|
||||
.venv
|
||||
.env
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11
|
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# LLMRPG
|
||||
|
||||
A simple 2D RPG game using LLMs to control NPCs behavior.
|
||||
|
||||

|
||||
|
||||
## 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
2
env.example
Normal file
@ -0,0 +1,2 @@
|
||||
OLLAMA_URL=http://localhost:11434
|
||||
OLLAMA_MODEL=llama3.1:latest
|
14
pyproject.toml
Normal file
14
pyproject.toml
Normal 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
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
noise
|
||||
numpy
|
||||
ollama
|
||||
pygame
|
||||
python-dotenv
|
||||
requests
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
20
src/config/colors.py
Normal file
20
src/config/colors.py
Normal 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
10
src/config/settings.py
Normal 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
65
src/effects/weather.py
Normal 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
671
src/entities/npc.py
Normal 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
51
src/entities/player.py
Normal 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
|
45
src/entities/world_object.py
Normal file
45
src/entities/world_object.py
Normal 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
508
src/main.py
Normal 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
148
src/menu/main_menu.py
Normal 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
25
src/ui/footer_bar.py
Normal 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
57
src/ui/header_bar.py
Normal 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
92
src/utils/dialog.py
Normal 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
9
src/utils/memory.py
Normal file
@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class Memory:
|
||||
event: str
|
||||
timestamp: float
|
||||
location: Tuple[float, float]
|
53
src/utils/name_generator.py
Normal file
53
src/utils/name_generator.py
Normal 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
114
src/utils/save_manager.py
Normal 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
26
src/world/camera.py
Normal 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
467
src/world/world.py
Normal 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
366
uv.lock
generated
Normal 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 },
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user