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") tee = Column(String, default="White") 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") 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 tee: str = "White" @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", tee=req.tee) 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, "tee": round_obj.tee} @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, "tee": round_obj.tee, "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": round_obj.tee } }) 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")