Press Office

Overview
Press Office is a mobile simulation game where you play as a press secretary trying to survive a full four-year term. You manage press conferences, respond to journalists, maintain relationships with the cabinet, and try to keep approval ratings from falling apart.
I built this project to push beyond web development and create a complete product end to end. That meant designing the game systems, building for mobile, handling offline data, and figuring out how to manage a large amount of narrative content as a solo developer.
The game currently includes 210+ llm-authored situations across seven categories: economy, security, ethics, governance, environment, foreign affairs, and domestic policy. Each situation includes structured outcomes, press exchanges, and entity preferences that shape how events play out.
Core Gameplay Systems
At the center of the game is a loop built around press conferences and evolving situations.
Each level represents a month. During that time, the player:
- Reviews active situations
- Responds to journalist questions
- Chooses how to communicate on behalf of the administration
- Watches outcomes play out based on those decisions
Each response is tied to a specific strategy such as deflecting, reassuring, challenging, admitting, or denying. These choices impact:
- Relationships with journalists and cabinet members
- Approval ratings across voter subgroups
- The probability of different outcomes
The goal is not to “win” every situation, but to manage tradeoffs. Protecting approval might damage trust. Being transparent might create short term backlash. The systems are designed so that there is rarely a perfect answer.
Game Engine Architecture
The entire game logic layer lives in a standalone module under lib/game/. This includes files like press-conference.ts, exchange-tree.ts, situations.ts, consequences.ts, relationships.ts, and outcomes.ts.
All of this logic is implemented as pure functions. There are no imports from React, WatermelonDB, or any framework code. Each function takes typed inputs and returns typed outputs with no side effects.
For example, calculateOutcomeWeights() takes base outcome weights and exchange results, then returns adjusted probabilities based on player decisions. It does not read from a database or mutate shared state.
This separation was intentional. It makes the game engine portable, easy to test, and independent from the UI layer. The test files in the same directory run without any mocking of framework internals, which allowed me to iterate quickly on game balance and logic without touching the app itself.
Content Pipeline (Authoring Tooling)
A game like this needs a lot of structured content. To support that, I built a generation pipeline in scripts/gen-situation/ that assists with authoring new scenarios.
The pipeline runs four sequential steps:
PlanningStep
Generates the high-level concept of a situation: title, description, category, which cabinet members and voter subgroups are involved, and which publications will cover it.
PreferencesStep
Defines what each entity wants. This includes the president’s preferred response strategy, cabinet member preferences, and optional “authorized content” such as sensitive information that can be revealed under certain conditions.
OutcomesStep
Generates 2 to 4 possible outcomes per situation. Each outcome includes a base probability weight and its impact on relationships and approval scores using a defined weight range.
ExchangesStep
Builds the press conference itself. Each publication gets an editorial angle and a multi-level question tree, where each question has multiple answer options with defined impacts and outcome modifiers.
Each step uses structured JSON outputs, which guarantees that responses are parseable and consistent. The final output is validated against Zod schemas in lib/schemas/, enforcing rules like probability weights summing correctly and text staying within mobile-friendly limits.
The pipeline also tracks token usage and cost per generation.
Importantly, this system is used as a development tool. The final content in the game is hand-curated TypeScript files, not raw model output. The pipeline helps with structure and scale, but the end result is edited and controlled.
Offline-First Architecture
The entire game runs locally on the device using SQLite via WatermelonDB.
This was a deliberate decision. It removes the need for a backend, keeps performance fast, and allows the game to work fully offline.
The data layer is split into two parts:
- Persistent state stored in WatermelonDB (games, relationships, progress)
- Ephemeral UI state handled by Zustand (modals, animations, temporary state)
Components observe database objects directly, which keeps the UI in sync without manual data fetching or refresh logic.
Mobile UI and Component System
For UI, I wanted something that felt native but still allowed for fast iteration.
I used React Native Reusables as a base for accessible primitives and layered NativeWind on top for styling. This gave me a workflow similar to Tailwind on the web while still producing a mobile-first interface.
The component structure is split into:
- UI primitives for building blocks
- Shared components for reusable game elements
- Screen-level components for layouts and flows
This made it easy to iterate quickly while keeping the codebase organized.
V2 Prototype (Interaction Redesign)
After building the initial version, I realized the menu-based interaction model did not fully take advantage of mobile.
I built a separate v2 prototype under components/v2/ with a playground at app/v2-playground.tsx to explore a different direction.
Key changes:
- Swipe-based interaction instead of menu selection, using gesture handling and animation libraries
- Four high-level meters (Public Approval, Media Trust, Cabinet Loyalty, National Stability) replacing granular relationship tracking
- A heat system where evasive answers build pressure and can trigger forced negative outcomes
- A political capital resource that can be spent during a briefing phase to influence situations
- A three-phase session loop: Briefing → Press Conference → Results
This prototype represents a shift toward a more tactile, mobile-native experience. It shows how I approach iteration. I do not just build a system and move on. I revisit it, identify what is not working, and explore alternatives at both the design and technical level.
Testing and Production Setup
Even as a solo project, I treated this like a production app.
End-to-end tests are written with Maestro and run on real simulators for both iOS and Android. These tests cover core flows like starting a game, progressing through levels, and verifying that data persists correctly.
The CI pipeline runs on every push and includes:
- Type checking
- Linting
- Unit tests
- E2E tests
- Bundle size checks
I also integrated error tracking and analytics with proper consent handling.
Development Challenges
Content consistency at scale
Getting consistent, useful output required more than just prompting. I needed validation layers, structure, and iteration loops to maintain quality across hundreds of situations.
Designing meaningful systems
Balancing relationships, approval, and outcomes was the hardest part. Small changes to weights could significantly affect the experience, so I needed systems that were flexible but predictable.
Offline relational data modeling
Managing interconnected entities in an offline database required careful schema design and migration planning.
Mobile testing reliability
Running stable E2E tests across iOS and Android required handling timing issues, system dialogs, and platform differences.
This project reflects how I approach building software. I design the systems carefully, build the tools needed to scale them, and treat it like something real from day one.