Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tactics League Liveops

A server-side backend and Unity 2D client for a football tactics game. Players choose a tactic, the server simulates the match, and the result feeds into a quest system, a coin economy, and a leaderboard. The Unity client is a work in progress — the server is fully functional, the UI is built, and I'm working on wiring them together.

I built this because I'm a football fan who wanted to understand how game backends are structured, particularly the kind of systems that run in the background of a live game — match results, player progression, rewards, rankings. It's also my first time working with Unity, so this project spans both backend development and game client work.


How it works

When a player submits a match, they send three things: their player ID, an opponent team ID, and a tactic choice (Attack, Balanced, or Defend). That's it. They do not send a score or a seed.

The server generates a random seed, runs the simulation, and returns the full result. Keeping the seed server-side means the client cannot manipulate the outcome — a modified client or a replayed HTTP request still gets a legitimately generated match result.

The tactic choice has a real effect on the match:

  • Attack — 6 player shot attempts, 5 for the opponent. More chances to score, but more exposure.
  • Balanced — 4 vs 4. Neutral.
  • Defend — 3 vs 2. Fewer shots overall, but the opponent is suppressed more than you are.

Each shot attempt has a 40% conversion chance. Goals from both sides determine the outcome (Win, Draw, Loss).

After the match resolves, the server emits a list of typed events — MatchPlayed, GoalsScored, MatchWon, CleanSheet — and passes them to the quest engine. The quest engine processes each active quest independently against that event list, updates progress, and awards coins if a quest completes.


Architecture

The server is split into two projects:

TacticsLeague.Domain contains MatchSimulator and QuestEngine. Neither class has any dependency on ASP.NET Core or on any database. They take inputs, return outputs. This means they can be unit-tested directly, and they won't change if the API layer or storage layer ever changes.

TacticsLeague.Api contains the controllers and GameState. GameState is infrastructure — it holds in-memory player data and wires the domain logic together. The three controllers are thin: they validate input, call into domain or state, and return the result.

The separation was intentional. I wanted MatchSimulator and QuestEngine to be ignorant of each other as well. The simulator doesn't know what quests exist. The quest engine doesn't know how goals are scored. The event list is the contract between them — if I add a new quest type, I add a new MatchEventType and nothing else in the simulator changes.

State

State lives in ConcurrentDictionary collections on a singleton GameState. This is a deliberate MVP tradeoff: there's no database, and state is lost on restart. The concurrency primitives make individual reads and writes safe, but ApplyMatch is not fully atomic across all three stores (quest progress, coins, leaderboard). For the current scope this is acceptable, and it's acknowledged in docs/api-design.md.


Quest system

There are four quests in the current catalog:

Quest Trigger Target Reward
Score 5 Goals GoalsScored event 5 100 coins
Win 3 Matches MatchWon event 3 150 coins
Keep 2 Clean Sheets CleanSheet event 2 120 coins
Play 10 Matches MatchPlayed event 10 80 coins

Rewards are awarded exactly once. QuestProgress carries separate IsCompleted and RewardClaimed flags — if a quest is already completed or its reward already claimed, QuestEngine returns zero coins and leaves the record unchanged.


Leaderboard

Updated after every match. Sorted by: points descending, then wins, then goals scored, then matches played ascending (as a tiebreaker that favors players who reached the same point total in fewer games). Points: Win = 3, Draw = 1, Loss = 0.


API endpoints

Method Path Description
POST /api/matches/simulate Simulate a match and update player state
GET /api/players/{playerId}/quests Get quest progress for a player
GET /api/leaderboard Get the full leaderboard

Full request/response shapes are in docs/api-design.md.

OpenAPI JSON is available at /openapi/v1.json when running in Development mode.


Code structure

tactics-league-liveops/
├── server/
│   ├── TacticsLeague.Domain/
│   │   ├── Matches/        # MatchSimulator, MatchInput, MatchResult, Tactic, MatchEvent
│   │   └── Quests/         # QuestEngine, QuestDefinition, QuestProgress, QuestUpdateResult
│   ├── TacticsLeague.Api/
│   │   ├── Controllers/    # MatchesController, PlayersController, LeaderboardController
│   │   ├── GameState.cs    # In-memory state, quest catalog, leaderboard
│   │   └── Program.cs      # DI registration, middleware
│   └── TacticsLeague.Tests/
│       ├── MatchSimulatorTests.cs
│       └── QuestEngineTests.cs
└── client-unity/
    └── TacticsLeagueClient/   # Unity 6 2D project (UI built, API integration in progress)

Tests

15 unit tests across MatchSimulator and QuestEngine.

MatchSimulatorTests covers: determinism (same seed → same result), correct event emission for goals, wins, and clean sheets, and tactic passthrough on the result.

QuestEngineTests covers: matching vs non-matching events, multiple events summed, progress capped at target, reward granted on first completion, no double-reward on already-completed quests, RewardClaimed flag persistence, and an ArgumentOutOfRangeException on negative event counts.

cd server
dotnet test

Tools and stack

Layer Technology
Server .NET 10, ASP.NET Core
Testing xUnit 2.x
Unity client Unity 6000.4.10f1, URP 2D
UI TextMeshPro

Screenshots / video

Coming soon — server is running, Unity UI is built, currently connecting the two.


What I learned

Designing MatchInput and MatchResult took more thought than I expected. The question of what belongs in each type is really a question of trust: what does the client get to decide, and what does the server have to own? Once I framed it that way, the separation became clearer — the client expresses intent, the server resolves the outcome.

The tactic system also needed more care than the initial version had. The first implementation gave both the player and opponent the same number of shot attempts regardless of which tactic was selected, which meant the choice had no effect. The current version gives Attack a slight offensive edge at the cost of more opponent attempts, while Defend suppresses both sides but gives the opponent fewer chances than the player.

The event-driven quest system was the piece I'm happiest with. Keeping MatchSimulator and QuestEngine ignorant of each other means either can change independently. It also made the tests easier to write — I can test quest logic by constructing any event list directly, without needing to run a match.


What's next

  • Connect the Unity client to the API (UnityWebRequest calls from GameDemoController)
  • Add more match events and quest types
  • Tactic effects on the player's squad composition or player stats
  • Persist state to a database so it survives server restarts
  • CORS configuration for the Unity build
  • Load the quest catalog from config rather than hardcoding it in GameState

How to run

Prerequisites

Tool Version
.NET SDK 10.0+
Unity Editor 6000.4.10f1

Server

cd server
dotnet restore
dotnet build
cd server/TacticsLeague.Api
dotnet run

API runs at http://localhost:5118 and https://localhost:7158.

Unity client

  1. Open Unity Hub, click Add, and select client-unity/TacticsLeagueClient.
  2. Open with Unity 6000.4.10f1.
  3. Open Assets/Scenes/MainScene.Unity.
  4. Start the server first, then press Play.

The client currently shows placeholder data while API integration is in progress.

About

Football tactics game with a .NET 10 match-simulation server, event-driven quest system, coin economy, leaderboard, and Unity 6 2D client.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages