Field note
From UmaOptimizer to UmaCompanion: The Earlier Architecture
How UmaCompanion began as a Python and PySide6 desktop app before its current Rust and TypeScript rebuild.
Update, August 2026: Active development of UMA Companion has ended. The project remains available as an archive, and the new retrospective explains what it became, what I learned, and why I chose to sunset it. This post remains a snapshot of the earlier stage of development.
Historical project context: UmaOptimizer has since become UmaCompanion, and the current application was rebuilt with Rust and a TypeScript/TSX interface. This post documents the earlier Python, PySide6, Next.js, and FastAPI architecture as it existed at the time.
As a developer, I love building tools that solve complex, real-world problems for gaming communities. If you play Uma Musume: Pretty Derby, you know how mathematically intense it gets. Trying to build the perfect support card deck requires juggling hundreds of effects, character aptitudes, and race courses. It's essentially a giant optimization problem.
So, I decided to build UmaOptimizer (originally known as the Umamusume Support Card Manager).
This post breaks down the development journey—from the initial desktop prototypes and technical fails, to the implementation of a beam-search algorithm, and the eventual expansion into a full-stack web platform.
🏗️ Building the App: The Journey to PySide6
The application didn't start in its current state. To ensure a snappy, offline experience capable of heavy mathematical calculations, I initially built a Python desktop prototype using CustomTkinter. However, I quickly hit a ceiling in rendering performance, custom styling, and advanced UI interactions (like drag-and-drop for deck building).
The Migration: I made the tough decision to completely rewrite the GUI using PySide6 (Qt6).
It was a massive undertaking. I replaced the entire frontend and built a centralized design system from scratch (gui/design_system.py and gui/theme.py). By using design tokens and PySide6 factory functions, I created the "AETHER" design system—a cohesive dark-mode UI with void depth layers and indigo accents that escapes the typical "clunky" feel of native Qt wrappers.
It wasn't easy (fighting with PySide6 enum changes and custom drag-and-drop coordinate math), but the result was a blazingly fast, modern desktop experience.
✨ The Features & The Beam-Search Optimizer
The application is packed with features designed specifically for the complex mechanics of the game:
- Card Library & Collection Tracker: Users browse an up-to-date catalog of 541+ support cards. A local SQLite database tracks their specific collection, including ownership and card levels.
- Race Calendar & Track Browser: A visual dashboard that cross-references character aptitudes against upcoming race schedules and explores track phases and skill activation zones.
- The Beam-Search Optimizer: The absolute core of the platform. Instead of relying on guesswork or generic tier lists, I implemented a beam-search algorithm. This mathematically evaluates thousands of permutations of a 6-card deck against specific scenario requirements to find the true global maxima of deck potential.
💥 The Fails and Technical Challenges
Building a local-first application is full of unique challenges. Here are a few "fails" and how I engineered my way out of them:
Fail 1: The "Wiped Save Data" Problem
Initially, whenever the game released new cards, I would push an update to the SQLite database. However, users were furious when they realized downloading the new database overwrote their local save data (their carefully curated decks and owned cards).
The Fix (The "Option B" Distribution Model): I decoupled the application code from the catalog data. Now, the app ships in two parts: a slim executable (.exe) and a separate Catalog Data Pack (.zip).
I wrote a highly custom SQLite synchronization engine (sync_from_seed()). When the user downloads a new Data Pack, the app safely merges the new catalog tables (cards, effects, tracks) while strictly preserving the user tables (owned collections, decks).
Fail 2: Distributing Playwright to End Users
At first, I tried to bundle the Playwright Chromium scraper directly into the PyInstaller .exe so users could scrape their own updates. The executable bloated to an absurd size, and Windows Defender constantly flagged it.
The Fix: I moved all scraping logic strictly to the maintainer side. The end-users never scrape anything. I use GitHub Actions to run the Playwright scrapers automatically, build the lightweight Data Pack zip, and serve it directly to the users.
🌐 Expanding to the Web: umaapp.kiyreload.com
While the offline-first desktop app is incredibly powerful, I realized that many players wanted access to their deck optimizations and card databases on the go, without installing an .exe.
To solve this, I expanded the project into a full-stack web platform at umaapp.kiyreload.com:
- The Frontend: Built with Next.js (React) and styled to perfectly match the desktop app's AETHER design system. It introduces the Community Hub, where users can share decks and view global tier lists.
- The Backend: A FastAPI (Python) server using SQLAlchemy and Alembic.
By abstracting the core optimizer and database query logic out of the PySide6 app, I was able to plug it directly into the FastAPI backend. It serves the exact same optimization logic and card data APIs as the desktop core, ensuring a 1:1 parity and a single source of truth between the two platforms.
🚀 Takeaways
Building UmaOptimizer has been a massive exercise in local-first design and full-stack development. Managing SQLite schema migrations on client machines, designing a custom PySide6 theme engine from scratch, and eventually porting the core logic to a Next.js/FastAPI web platform taught me a massive amount about software distribution and architecture.
At the time of writing, the project was fully packaged with CI/CD and actively used by the community. The later retrospective covers how the project continued to evolve after this earlier architecture.