Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
286 lines
11 KiB
Python
286 lines
11 KiB
Python
from fastapi import FastAPI, HTTPException
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, ForeignKey
|
|
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
|
|
import os
|
|
|
|
DATABASE_URL = "sqlite:///./backend/golf.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
|
|
Base = declarative_base()
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String, nullable=False)
|
|
handicap = Column(Integer, default=0)
|
|
|
|
class Course(Base):
|
|
__tablename__ = "courses"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String, nullable=False)
|
|
|
|
class Hole(Base):
|
|
__tablename__ = "holes"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
course_id = Column(Integer, ForeignKey("courses.id"))
|
|
number = Column(Integer, nullable=False)
|
|
par = Column(Integer, nullable=False)
|
|
yardage = Column(Integer, nullable=False)
|
|
yardage_meters = Column(Integer, default=0)
|
|
tee = Column(String, default="White")
|
|
|
|
class Round(Base):
|
|
__tablename__ = "rounds"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"))
|
|
course_id = Column(Integer, ForeignKey("courses.id"))
|
|
status = Column(String, default="in_progress")
|
|
|
|
class HoleScore(Base):
|
|
__tablename__ = "hole_scores"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
round_id = Column(Integer, ForeignKey("rounds.id"))
|
|
hole_number = Column(Integer, nullable=False)
|
|
strokes = Column(Integer, default=0)
|
|
skipped = Column(Boolean, default=False)
|
|
completed = Column(Boolean, default=False)
|
|
fairway_hit = Column(Boolean, default=False)
|
|
gir = Column(Boolean, default=False)
|
|
putts_green = Column(Integer, default=0)
|
|
putts_off_green = Column(Integer, default=0)
|
|
penalty_water = Column(Boolean, default=False)
|
|
penalty_ob = Column(Boolean, default=False)
|
|
penalty_lost = Column(Boolean, default=False)
|
|
bunker_greenside = Column(Boolean, default=False)
|
|
bunker_fairway = Column(Boolean, default=False)
|
|
unfinished = Column(Boolean, default=False)
|
|
|
|
class Putt(Base):
|
|
__tablename__ = "putts"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
hole_score_id = Column(Integer, ForeignKey("hole_scores.id"))
|
|
location = Column(String, nullable=False)
|
|
|
|
class Penalty(Base):
|
|
__tablename__ = "penalties"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
hole_score_id = Column(Integer, ForeignKey("hole_scores.id"))
|
|
type = Column(String, nullable=False)
|
|
|
|
class Bunker(Base):
|
|
__tablename__ = "bunkers"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
hole_score_id = Column(Integer, ForeignKey("hole_scores.id"))
|
|
location = Column(String, nullable=False)
|
|
|
|
User.rounds = relationship("Round", back_populates="user")
|
|
Round.user = relationship("User", back_populates="rounds")
|
|
Round.hole_scores = relationship("HoleScore", back_populates="round")
|
|
HoleScore.round = relationship("Round", back_populates="hole_scores")
|
|
HoleScore.putts = relationship("Putt", back_populates="hole_score")
|
|
Putt.hole_score = relationship("HoleScore", back_populates="putts")
|
|
|
|
# Recreate DB for prototype if schema changed
|
|
if os.path.exists("backend/golf.db"):
|
|
os.remove("backend/golf.db")
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
app = FastAPI()
|
|
|
|
def init_mock():
|
|
db = SessionLocal()
|
|
if db.query(Course).first():
|
|
db.close()
|
|
return
|
|
course = Course(name="Mock Valley")
|
|
db.add(course)
|
|
db.commit()
|
|
db.refresh(course)
|
|
holes = [
|
|
Hole(course_id=course.id, number=1, par=4, yardage=350, yardage_meters=320, tee="White"),
|
|
Hole(course_id=course.id, number=2, par=3, yardage=180, yardage_meters=165, tee="White"),
|
|
Hole(course_id=course.id, number=3, par=5, yardage=520, yardage_meters=480, tee="Yellow"),
|
|
Hole(course_id=course.id, number=4, par=4, yardage=390, yardage_meters=360, tee="White"),
|
|
Hole(course_id=course.id, number=5, par=3, yardage=160, yardage_meters=145, tee="Red"),
|
|
Hole(course_id=course.id, number=6, par=4, yardage=410, yardage_meters=375, tee="Blue"),
|
|
Hole(course_id=course.id, number=7, par=5, yardage=540, yardage_meters=495, tee="Yellow"),
|
|
Hole(course_id=course.id, number=8, par=4, yardage=370, yardage_meters=340, tee="White"),
|
|
Hole(course_id=course.id, number=9, par=3, yardage=190, yardage_meters=175, tee="Red"),
|
|
]
|
|
db.add_all(holes)
|
|
db.commit()
|
|
db.close()
|
|
|
|
init_mock()
|
|
|
|
class SignupRequest(BaseModel):
|
|
name: str
|
|
handicap: int = 0
|
|
|
|
@app.post("/signup")
|
|
def signup(req: SignupRequest):
|
|
db = SessionLocal()
|
|
user = User(name=req.name, handicap=req.handicap)
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
db.close()
|
|
return {"id": user.id, "name": user.name, "handicap": user.handicap}
|
|
|
|
class StartRoundRequest(BaseModel):
|
|
user_id: int
|
|
course_id: int
|
|
|
|
@app.post("/round")
|
|
def start_round(req: StartRoundRequest):
|
|
db = SessionLocal()
|
|
round_obj = Round(user_id=req.user_id, course_id=req.course_id, status="in_progress")
|
|
db.add(round_obj)
|
|
db.commit()
|
|
db.refresh(round_obj)
|
|
holes = db.query(Hole).filter(Hole.course_id == req.course_id).all()
|
|
for h in holes:
|
|
hs = HoleScore(round_id=round_obj.id, hole_number=h.number, strokes=0, skipped=False, completed=False, fairway_hit=False, gir=False, putts_green=0, putts_off_green=0, unfinished=False)
|
|
db.add(hs)
|
|
db.commit()
|
|
db.close()
|
|
return {"id": round_obj.id, "status": round_obj.status}
|
|
|
|
@app.get("/course")
|
|
def get_course():
|
|
db = SessionLocal()
|
|
course = db.query(Course).first()
|
|
holes = db.query(Hole).filter(Hole.course_id == course.id).all()
|
|
db.close()
|
|
return {
|
|
"id": course.id,
|
|
"name": course.name,
|
|
"holes": [{"number": h.number, "par": h.par, "yardage": h.yardage, "yardage_meters": h.yardage_meters, "tee": h.tee} for h in holes]
|
|
}
|
|
|
|
@app.get("/round/{round_id}")
|
|
def get_round(round_id: int):
|
|
db = SessionLocal()
|
|
round_obj = db.query(Round).filter(Round.id == round_id).first()
|
|
if not round_obj:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail="Round not found")
|
|
hole_scores = db.query(HoleScore).filter(HoleScore.round_id == round_id).all()
|
|
result = {
|
|
"id": round_obj.id,
|
|
"user_id": round_obj.user_id,
|
|
"course_id": round_obj.course_id,
|
|
"status": round_obj.status,
|
|
"holes": []
|
|
}
|
|
for hs in hole_scores:
|
|
hole = db.query(Hole).filter(Hole.course_id == round_obj.course_id, Hole.number == hs.hole_number).first()
|
|
result["holes"].append({
|
|
"hole_score_id": hs.id,
|
|
"hole_number": hs.hole_number,
|
|
"strokes": hs.strokes,
|
|
"skipped": hs.skipped,
|
|
"completed": hs.completed,
|
|
"fairway_hit": hs.fairway_hit,
|
|
"gir": hs.gir,
|
|
"unfinished": hs.unfinished,
|
|
"putts_green": hs.putts_green,
|
|
"putts_off_green": hs.putts_off_green,
|
|
"penalty_water": hs.penalty_water,
|
|
"penalty_ob": hs.penalty_ob,
|
|
"penalty_lost": hs.penalty_lost,
|
|
"bunker_greenside": hs.bunker_greenside,
|
|
"bunker_fairway": hs.bunker_fairway,
|
|
"hole_info": {
|
|
"par": hole.par if hole else None,
|
|
"yardage_meters": hole.yardage_meters if hole else 0,
|
|
"tee": hole.tee if hole else "White"
|
|
}
|
|
})
|
|
db.close()
|
|
return result
|
|
|
|
class UpdateHoleRequest(BaseModel):
|
|
strokes: int = 1
|
|
skipped: bool = False
|
|
completed: bool = False
|
|
fairway_hit: bool = False
|
|
unfinished: bool = False
|
|
putts_green: int = 0
|
|
putts_off_green: int = 0
|
|
penalty_water: bool = False
|
|
penalty_ob: bool = False
|
|
penalty_lost: bool = False
|
|
bunker_greenside: bool = False
|
|
bunker_fairway: bool = False
|
|
|
|
@app.post("/hole/{hole_score_id}")
|
|
def update_hole(hole_score_id: int, req: UpdateHoleRequest):
|
|
db = SessionLocal()
|
|
hs = db.query(HoleScore).filter(HoleScore.id == hole_score_id).first()
|
|
if not hs:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail="Hole score not found")
|
|
hs.strokes = req.strokes
|
|
hs.skipped = req.skipped
|
|
hs.completed = req.completed
|
|
hs.fairway_hit = req.fairway_hit
|
|
hs.unfinished = req.unfinished
|
|
hs.putts_green = req.putts_green
|
|
hs.putts_off_green = req.putts_off_green
|
|
hs.penalty_water = req.penalty_water
|
|
hs.penalty_ob = req.penalty_ob
|
|
hs.penalty_lost = req.penalty_lost
|
|
hs.bunker_greenside = req.bunker_greenside
|
|
hs.bunker_fairway = req.bunker_fairway
|
|
|
|
round_obj = db.query(Round).filter(Round.id == hs.round_id).first()
|
|
hole = db.query(Hole).filter(Hole.course_id == round_obj.course_id, Hole.number == hs.hole_number).first()
|
|
if hole:
|
|
total_putts = hs.putts_green + hs.putts_off_green
|
|
all_green = hs.putts_off_green == 0
|
|
hs.gir = (not hs.unfinished) and (hs.strokes - total_putts) <= (hole.par - 2) and all_green
|
|
|
|
db.commit()
|
|
db.refresh(hs)
|
|
db.close()
|
|
return {
|
|
"hole_score_id": hs.id,
|
|
"gir": hs.gir,
|
|
"strokes": hs.strokes,
|
|
"putts_green": hs.putts_green,
|
|
"putts_off_green": hs.putts_off_green,
|
|
"unfinished": hs.unfinished
|
|
}
|
|
|
|
@app.get("/stats/{round_id}")
|
|
def get_stats(round_id: int):
|
|
db = SessionLocal()
|
|
hs_list = db.query(HoleScore).filter(HoleScore.round_id == round_id).all()
|
|
total_strokes = sum(hs.strokes for hs in hs_list if not hs.skipped and not hs.unfinished)
|
|
total_putts = sum((hs.putts_green + hs.putts_off_green) for hs in hs_list if not hs.skipped and not hs.unfinished)
|
|
fairways = sum(1 for hs in hs_list if hs.fairway_hit and not hs.skipped and not hs.unfinished)
|
|
girs = sum(1 for hs in hs_list if hs.gir and not hs.skipped and not hs.unfinished)
|
|
penalties = sum(
|
|
(1 if hs.penalty_water else 0) + (1 if hs.penalty_ob else 0) + (1 if hs.penalty_lost else 0)
|
|
for hs in hs_list if not hs.skipped and not hs.unfinished
|
|
)
|
|
bunker_greenside = sum(1 for hs in hs_list if hs.bunker_greenside and not hs.skipped and not hs.unfinished)
|
|
bunker_fairway = sum(1 for hs in hs_list if hs.bunker_fairway and not hs.skipped and not hs.unfinished)
|
|
db.close()
|
|
return {
|
|
"total_strokes": total_strokes,
|
|
"total_putts": total_putts,
|
|
"fairways_hit": fairways,
|
|
"gir": girs,
|
|
"penalty_strokes": penalties,
|
|
"bunker_greenside": bunker_greenside,
|
|
"bunker_fairway": bunker_fairway,
|
|
"holes_played": len([h for h in hs_list if not h.skipped and not h.unfinished])
|
|
}
|
|
|
|
app.mount("/", StaticFiles(directory="frontend", html=True), name="static")
|