OverviewI built a tic-tac-toe project that combines a playable web app with a reinforcement-learning side: the src/ area holds a self-play training pipeline and Q-learning-style agents for research and experimentation, while the shipped experience lets people play against AI opponents and see how game state is sent to a server for smarter moves. The goal was to turn a simple 3x3 game into something that shows both classic game AI and modern full-stack delivery, so visitors get a real product — not only a notebook or script.
The app uses a multi-page flow with React Router: home with name entry and mode choice (human vs AI or two humans), a dedicated game screen with scores and reset, plus settings, match history, and an about page. I focused on a cohesive retro phone shell layout, optional background music and sound effects, slide notifications for turns and errors, a win modal, and achievements tied to play patterns. Difficulty maps to different AI behaviors (easy random play, medium heuristics, hard minimax-style play), and history plus achievements persist in the browser so the experience feels continuous across sessions.
On the front end I used React with Vite, Redux Toolkit for game, settings, history, and achievements, and localStorage to persist everything except the in-progress board. The backend is a Flask REST API with CORS tuned for GitHub Pages, local Vite ports, and deployment hosts; it exposes health, move, and validate endpoints and reuses a shared Python tic-tac-toe engine. CI workflows build the SPA and deploy static assets to GitHub Pages while the API can run separately (for example on Render), with the front end pointing at a configurable base URL through environment variables — no traditional database, since state lives on the client.
A practical challenge was hosting the UI and API on different origins: browsers enforce CORS, and the static site can outlive or lose contact with the API. I addressed this by explicitly allowing the right origins on the Flask side and by handling failed fetch calls in the game logic: if the API is unreachable, the client shows a clear message and falls back to a simple random move so the game still runs. That kept the demo reliable for portfolio viewers even when the backend sleeps or errors, without blocking the whole interface.