Initial commit

This commit is contained in:
Micha R. Albert 2025-05-28 10:26:04 -04:00
commit 25ba7968d5
Signed by: mra
SSH key fingerprint: SHA256:2JB0fGfy7m2HQXAzvSXXKm7wPTj9Z60MOjFOQGM2Y/E
7 changed files with 381 additions and 0 deletions

View file

15
src/random_access/cli.py Normal file
View file

@ -0,0 +1,15 @@
import click
import uvicorn
from random_access.main import app
@click.group()
def cli():
"""Random Access Server CLI."""
pass
@cli.command()
@click.option("--host", default="127.0.0.1", help="Host to bind to.")
@click.option("--port", default=8000, help="Port to bind to.")
def run(host, port):
"""Run the FastAPI app."""
uvicorn.run(app, host=host, port=port)

15
src/random_access/db.py Normal file
View file

@ -0,0 +1,15 @@
from sqlmodel import SQLModel, create_engine, Session
DATABASE_URL = "sqlite:////home/micha/Documents/random-access/database.db" # sync SQLite URL
# connect_args needed for SQLite to allow multithreaded access
connect_args = {"check_same_thread": False}
engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args)
def init_db():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session

60
src/random_access/main.py Normal file
View file

@ -0,0 +1,60 @@
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import FastAPI, Depends, HTTPException, Header
from sqlmodel import select, Session
from .db import get_session, init_db
from .models import Player, World
from secrets import token_urlsafe
from argon2 import PasswordHasher
hasher = PasswordHasher()
@asynccontextmanager
async def lifespan(_: FastAPI):
init_db()
yield
app = FastAPI(lifespan=lifespan)
@app.post("/players/")
def create_player(player: Player, session: Session = Depends(get_session)) -> Player:
session.add(player)
session.commit()
session.refresh(player)
return player
@app.get("/players/location")
def read_player_location(
api_token: Annotated[str | None, Header()], session: Session = Depends(get_session)
):
if not api_token:
raise HTTPException(status_code=401, detail="No API token provided")
hashed_token = hasher.hash(api_token)
results = session.exec(select(Player).where(Player.api_token == hashed_token))
if len(results.all()) != 1:
raise HTTPException(status_code=500, detail="Critical error, please report this!")
@app.get("/players/")
def read_players(session: Session = Depends(get_session)) -> list[dict[str, int | str]]:
players = [
{
"id": player.id,
"username": player.in_game_name,
"current_world_id": player.current_world_id,
"level": player.level,
}
for player in list(session.exec(select(Player)).all())
]
return players
@app.get("/worlds")
def read_worlds(session: Session = Depends(get_session)):
worlds = list(session.exec(select(World)).all())
return worlds

View file

@ -0,0 +1,73 @@
import datetime
from decimal import Decimal
from enum import Enum
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel
from sqlalchemy import Column, Integer
class ItemCategory(str, Enum):
ranged_weapon = "ranged_weapon"
melee_weapon = "melee_weapon"
armor_head = "armor_head"
armor_torso = "armor_torso"
armor_legs = "armor_legs"
armor_shoes = "armor_shoes"
potion = "potion"
throwable_potion = "throwable_potion"
special = "special"
shield = "shield"
resource = "resource"
class World(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
creator_id: Optional[int] = Field(default=None, foreign_key="player.id")
creator: Optional["Player"] = Relationship(
back_populates="personal_world", sa_relationship_kwargs={"foreign_keys": "[World.creator_id]"}
)
inhabitants: List["Player"] = Relationship(
back_populates="current_world", sa_relationship_kwargs={"foreign_keys": "[Player.current_world_id]"}
)
class Player(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
in_game_name: Optional[str]
slack_id: str = Field(index=True)
api_token: str = Field(index=True)
token_last_updated: datetime.datetime
pos_x: int = Field(default=0)
pos_y: int = Field(default=0)
inventory: List["Item"] = Relationship(back_populates="player")
level: int = Field(default=1, le=100)
xp: int = Field(default=0, lt=1000)
personal_world: Optional["World"] = Relationship(
back_populates="creator", sa_relationship_kwargs={"foreign_keys": "[World.creator_id]"}
)
current_world_id: Optional[int] = Field(default=None, foreign_key="world.id")
current_world: Optional["World"] = Relationship(
back_populates="inhabitants", sa_relationship_kwargs={"foreign_keys": "[Player.current_world_id]"}
)
class Item(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
category: ItemCategory
cost: Optional[Decimal] = None
can_be_sold: bool = Field(default=False)
is_breakable: bool = Field(default=False)
hitpoints_remaining: Optional[Decimal] = None
player_id: Optional[int] = Field(default=None, foreign_key="player.id")
player: Optional[Player] = Relationship(back_populates="inventory")