Skip to content

dylantcon/countertrak

Repository files navigation

The CounterTrak Application

Table of Contents

Development Team

  1. Dylan Connolly (dtc22h@fsu.edu)
  2. Zack Lima (zol21@fsu.edu)

Repository: https://github.com/dylantcon/countertrak

Introduction

The CounterTrak application represents an innovative approach to performance tracking for players of Valve Corporation's popular first-person-shooter Counter Strike 2 (CS2). Developed as a semester-long project for the COP4710 Theory and Structure of Databases course, this web-based informatics system harnesses the power of relational database management systems (RDBMS) to provide players with meaningful insights into their gameplay patterns and performance metrics.

What distinguishes CounterTrak from existing statistics tracking platforms is our focus on lightweight design coupled with sophisticated analytics. While services like HLTV.org and csgostats.gg offer comprehensive but resource-intensive tracking, CounterTrak prioritizes simplicity and accessibility without sacrificing analytical depth. Our application demonstrates that intelligent database design can yield powerful insights without requiring massive computational resources or complex infrastructure.

The system consists of several key components working in concert: an asynchronous HTTP server that receives game state data, a match manager that routes payloads to appropriate processors, a sophisticated database schema optimized for performance analysis, and an intuitive web interface that visualizes player statistics. Together, these components create a seamless pipeline from in-game actions to actionable performance insights, all powered by PostgreSQL and implemented through a Django-based Python backend system.

In the following sections, we detail our database design decisions, functionality implementation, system architecture, and the valuable lessons learned throughout the development process.

Motivations

The inspiration that sparked the creation of the CounterTrak application emanated from the team's appreciation for online gaming, and professional game development. Both Zach and Dylan cultivated an admiration for computers and computing concepts from an early age. These formative circumstances resulted in a natural gravitation towards the gaming community, with online gaming presenting itself as yet another opportunity to engage with the machines they found so captivating. They made many fond memories during these times, and explored the vast range of video-game genres and subgenres. Over time, they began to perceive gaming not just as a mere diversion, but a rich culture that naturally aligned with their systems-oriented cognitive profiles.

Modern games are immensely complex works of art, born from precise collaborative efforts characteristic of highly seasoned software engineers and computer scientists. The team's discovery of CS2's GSI system presented itself as an opportunity to exercise their collective appreciation for game-development concepts, by applying it towards a data-driven informatics system. The rich dataset facilitated by CS2 GSI was the ideal foundation for a database-powered application. Being highly motivated software developer aspirants, the team members wanted their academic work to be truly distinctive. As a result, CounterTrak was born.

Database Design

The foundation of CounterTrak's functionality lies in its carefully designed database structure. Our database design process began with identifying the key entities in the CS2 gameplay domain and establishing their relationships, followed by translating this conceptual model into a normalized relational schema suitable for implementation in PostgreSQL.

Entity-Relationship Model

To accurately capture the complex data generated by CS2's Game State Integration system, we developed an entity-relationship model that identifies eight core entities and their interrelationships. This model needed to account for the hierarchical nature of CS2 matches (matches containing rounds, rounds containing player states) while maintaining flexibility for analytical queries.

The entities we identified include:

  1. Users - CounterTrak account holders who access the application
  2. SteamAccounts - Steam identities linked to user accounts for authentication
  3. Matches - Individual CS2 games with metadata such as map, mode, and scores
  4. Rounds - Discrete gameplay units within matches
  5. PlayerRoundStates - Player status data during specific rounds
  6. Weapons - Reference data for all available CS2 weapons
  7. PlayerWeapons - Relationship entity tracking equipped weapons during gameplay
  8. PlayerMatchStats - Aggregate performance statistics for players in matches

These entities and their relationships are visualized in the comprehensive ER diagram below: counterTrak ERD This diagram illustrates several important design decisions. First, we established a clear separation between user accounts and Steam identities, allowing a single user to track multiple Steam accounts. Second, we implemented a hierarchical structure from matches to rounds to player states, enabling detailed temporal analysis. Third, we created a specialized entity for weapon tracking to support our advanced analytics functions.

Relational Schema Design

To transform our conceptual ER model into an implementable database design, we developed a detailed relational schema that preserves all entity relationships while optimizing for both data integrity and query performance. The resulting schema consists of eight tables with carefully defined primary and foreign key relationships.

$$\begin{aligned} &\text{Users}( \underline{steam\_id}, \text{username, password\_hash} ) \\ \\\ &\text{SteamAccounts}(\underline{steam\_id}, \text{user\_id, auth\_token, player\_name} ) \\\ &\exists \quad \text{FK: SteamAccount.user\_id references User}(\underline{user\_id})\\ \\\ &\text{Matches}( \underline{match\_id},\text{mode, map\_name, start\_timestamp, end\_timestamp,}\\ & \qquad \quad \; \text{rounds\_played, team\_ct\_score, team\_t\_score} ) \\ \\\ &\text{Rounds}( \underline{match\_id, round\_number}, \text{phase, timestamp, winning\_team,} \\ & \qquad \quad \; \text{win\_condition}) \\\ &\exists \quad \text{FK: Round.match\_id references Match}(\underline{match\_id}) \end{aligned}$$ $$\begin{aligned} &\text{PlayerRoundStates}( \underline{match\_id, round\_number, steam\_id} \text{, health, armor, } \\\ & \qquad \qquad \qquad \qquad \quad \text{money, equip\_value, round\_kills, state\_timestamp}) \\\ &\exists \quad \text{FK: PlayerRoundStates.(match\_id, round\_number) ref. Round}(\underline{match\_id, round\_number}) \\\ &\exists \quad \text{FK: PlayerRoundStates.steam\_id references SteamAccount}(\underline{steam\_id}) \\ \\\ &\text{Weapons}(\underline{weapon\_id}\text{, name, type, max\_clip}) \\ \\\ &\text{PlayerWeapons}(\underline{match\_id, round\_number, steam\_id, weapon\_id}\text{, state, ammo\_clip, state\_timestamp} \\\ & \qquad \qquad \qquad \quad \text{ammo\_reserve, paintkit}) \\\ &\exists \quad \text{FK: PlayerWeapons.(match\_id, round\_number, steam\_id) references} \\\ & \qquad \quad \; \; \text{PlayerRoundStates}(\underline{match\_id, round\_number, steam\_id}) \\\ &\exists \quad \text{FK: PlayerWeapons.weapon\_id references Weapons}(\underline{weapon\_id}) \\ \\\ &\text{PlayerMatchStats}(\underline{steam\_id, match\_id}\text{, kills, deaths, assists, mvps, score}) \\\ &\exists \quad \text{FK: PlayerMatchStats.steam\_id references SteamAccount}(\underline{steam\_id}) \\\ &\exists \quad \text{FK: PlayerMatchStats.match\_id references Match}(\underline{match\_id}) \end{aligned}$$

This schema includes several noteworthy features. We implemented composite primary keys in the Rounds, PlayerRoundStates, and PlayerWeapons tables to efficiently represent hierarchical relationships. The temporal aspect of game state tracking is captured through timestamps in appropriate tables, enabling precise sequence analysis. Additionally, we created a pre-populated Weapons table to serve as a reference for all in-game weapons, improving both performance and data consistency.

Normalization and Functional Dependencies

Our schema design adheres to Boyce-Codd Normal Form (BCNF), ensuring efficient data storage and minimizing anomalies. We identified and addressed the following functional dependencies:

  1. In the Users table, user_id functionally determines username and password_hash
  2. In the SteamAccounts table, steam_id functionally determines user_id, auth_token, and player_name
  3. In the Matches table, match_id functionally determines all match attributes
  4. In the Rounds table, the composite key (match_id, round_number) functionally determines round attributes
  5. In the PlayerRoundStates table, the composite key (match_id, round_number, steam_id) functionally determines player state attributes
  6. In the Weapons table, weapon_id functionally determines weapon attributes, with name as an alternative candidate key
  7. In the PlayerWeapons table, the composite key (match_id, round_number, steam_id, weapon_id) functionally determines weapon state attributes
  8. In the PlayerMatchStats table, the composite key (steam_id, match_id) functionally determines all player match statistics

This normalization approach provided several advantages for our application. It minimized data redundancy, reducing storage requirements for the potentially large volume of game state data. It eliminated update anomalies that could compromise data integrity during high-frequency state changes. Most importantly, it created a schema structure that naturally supported our advanced temporal sequence analysis for player performance patterns.

The schema also includes several integrity constraints beyond normal foreign key relationships. We implemented check constraints on valid value ranges for attributes like health, armor, and money. We created triggers to automatically update match scores when rounds are completed. Additionally, we developed custom validation logic in our Django models to ensure that incoming GSI data conforms to expected patterns before being committed to the database.

This comprehensive database design forms the foundation for CounterTrak's functionality, enabling both efficient storage of game state data and sophisticated analytical queries that reveal meaningful patterns in player performance.

Functionality Details

Basic Database Operations

CounterTrak implements a comprehensive set of database operations to manage game state data efficiently, focusing on the CRUD (Create, Read, Update, Delete) paradigm while handling the complexity of hierarchical and temporal relationships in our data model.

The record creation process is carefully orchestrated to maintain referential integrity across related entities. When a new match begins, the system creates a match record with initial state data, followed by round records and player state records as the match progresses. This insertion logic is particularly complex as it must handle transactional consistency across multiple related tables.

For example, when a new player action is detected through the GSI system, our application executes a series of interdependent database operations:

INSERT INTO stats_playerroundstate
(match_id, round_number, steam_account_id, health, armor,
 money, equip_value, round_kills, team, state_timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id

Within the CounterTrak system, record retrieval operations from a basic perspective center around duplicate record checks during match data ingress. Here is an example of a check for duplicate PlayerRoundState records, within backend/gsi/django_integration.py:

# ...
state_time = convert_unix_timestamp_to_datetime(player_state.state_timestamp)
with connection.cursor() as cursor:
	cursor.execute(
		"""
		SELECT COUNT(*) FROM stats_playerroundstate
		WHERE
			match_id = %s AND
			round_number = %s AND
			steam_account_id = %s AND
			state_timestamp = %s
		""",
		[
			match_id,
			round_number,
			player_state.steam_id,
			state_time
		]
	)
	return cursor.fetchone()[0] > 0
# ...

Updates are handled carefully to ensure data integrity, particularly for player statistics that change frequently during a match. The system uses atomic updates to modify specific fields without affecting unrelated data:

UPDATE stats_playermatchstat
SET 
    kills = EXCLUDED.kills,
    deaths = EXCLUDED.deaths,
    assists = EXCLUDED.assists,
    mvps = EXCLUDED.mvps,
    score = EXCLUDED.score

For data deletion, we implemented cascading delete operations that maintain referential integrity while efficiently removing all related records. This is particularly important for match deletion, which must clean up all associated round, player state, and weapon records:

DELETE FROM stats_playerweapon WHERE match_id = %s;
DELETE FROM stats_playerroundstate WHERE match_id = %s;
DELETE FROM stats_playermatchstat WHERE match_id = %s;
DELETE FROM matches_round WHERE match_id = %s;
DELETE FROM matches_match WHERE match_id = %s;

These basic operations form the foundation upon which our advanced analytical features are built, ensuring data integrity and performance even under high-volume data processing conditions.

Advanced Analytics Functions

The core of CounterTrak's value proposition lies in its sophisticated Performance Pattern Recognition engine, which goes beyond simple statistics aggregation to identify meaningful patterns in player performance. This engine represents our most advanced functionality and employs complex temporal sequence analysis to extract actionable insights.

The Performance Pattern Recognition engine focuses on three key analytical dimensions:

  1. Weapon Effectiveness Analysis: This component analyzes how effectively a player uses different weapons across various contexts. Unlike basic statistics systems that simply count kills per weapon, our system considers the temporal relationship between weapon activation and successful eliminations:
WITH weapon_performance AS (
    SELECT 
        m.map_name,
        w.name AS weapon_name,
        w.type AS weapon_type,
        COUNT(DISTINCT pw.match_id) AS matches_used,
        SUM(prs.round_kills) AS total_kills,
        COUNT(DISTINCT CASE WHEN prs.round_kills > 0 THEN prs.round_number ELSE NULL END) AS rounds_with_kills,
        COUNT(DISTINCT prs.round_number) AS total_rounds_used,
        ROUND(SUM(prs.round_kills)::numeric / NULLIF(COUNT(DISTINCT prs.round_number), 0), 3) AS kills_per_round,
        ROUND(COUNT(DISTINCT CASE WHEN prs.round_kills > 0 THEN prs.round_number ELSE NULL END)::numeric / 
              NULLIF(COUNT(DISTINCT prs.round_number), 0), 3) AS kill_consistency,
        ROUND(SUM(prs.round_kills)::numeric / NULLIF(SUM(prs.equip_value)/1000, 0), 3) AS kills_per_1000_spent
    FROM 
        stats_weapon w
    JOIN 
        stats_playerweapon pw ON w.weapon_id = pw.weapon_id
    JOIN 
        stats_playerroundstate prs ON
        pw.match_id = prs.match_id AND
        pw.round_number = prs.round_number AND
        pw.steam_account_id = prs.steam_account_id
    JOIN 
        matches_match m ON prs.match_id = m.match_id
    JOIN 
        stats_playermatchstat pms ON 
            prs.match_id = pms.match_id AND 
            prs.steam_account_id = pms.steam_account_id
    WHERE 
        pw.state = 'active'
        AND prs.steam_account_id = ${steam_id}
        AND w.type NOT IN ('Knife', 'Grenade', 'C4', 'Other')
    GROUP BY 
        m.map_name, w.name, w.type
    HAVING 
        COUNT(DISTINCT prs.round_number) >= 5
)
SELECT 
    map_name,
    weapon_name,
    weapon_type,
    matches_used,
    total_kills,
    kills_per_round,
    kill_consistency,
    kills_per_1000_spent,
    ROUND(
        (kills_per_round * 0.4) + 
        (kill_consistency * 0.3) + 
        (kills_per_1000_spent * 0.3)
    , 3) AS weapon_effectiveness_score,
    CASE 
        WHEN kills_per_round >= 0.75 AND kill_consistency >= 0.5 THEN 'S-Tier'
        WHEN kills_per_round >= 0.5 AND kill_consistency >= 0.4 THEN 'A-Tier'
        WHEN kills_per_round >= 0.3 AND kill_consistency >= 0.3 THEN 'B-Tier'
        WHEN kills_per_round >= 0.2 THEN 'C-Tier'
        ELSE 'D-Tier'
    END AS recommendation_tier
FROM 
    weapon_performance
ORDER BY 
    map_name, weapon_effectiveness_score DESC;
  • This query not only tracks kill statistics but calculates derived metrics like kill consistency (frequency of successful rounds), economic efficiency (kills per money spent), and a composite effectiveness score that weights these factors appropriately.
  1. Economic Impact Analysis: Our system examines how economic decisions affect performance by analyzing the relationship between equipment value, money management, and round outcomes. It analyzes various economic decisions including Full-Buy, Semi-Buy, Semi-Eco, and Eco strategies, and the round outcomes that result from them:
-- ECONOMIC ANALYSIS QUERIES
-- Demonstrates economic decision analysis and impact on performance
-- Economic round state analysis
WITH round_economics AS (
    SELECT 
        m.match_id,
        m.map_name,
        r.round_number,
        r.winning_team,
        CASE 
            WHEN AVG(prs.equip_value) < 1000 THEN 'Eco (< $1000)'
            WHEN AVG(prs.equip_value) BETWEEN 1000 AND 2500 THEN 'Semi-Eco ($1000-$2500)'
            WHEN AVG(prs.equip_value) BETWEEN 2501 AND 4000 THEN 'Semi-Buy ($2501-$4000)'
            WHEN AVG(prs.equip_value) > 4000 THEN 'Full Buy (> $4000)'
        END AS economic_state,
        AVG(prs.money) AS avg_remaining_money,
        AVG(prs.equip_value) AS avg_equip_value,
        SUM(prs.round_kills) AS total_kills,
        COUNT(DISTINCT prs.steam_account_id) AS player_count
    FROM 
        matches_match m
    JOIN 
        matches_round r ON m.match_id = r.match_id
    JOIN 
        stats_playerroundstate prs ON 
            r.match_id = prs.match_id AND 
            r.round_number = prs.round_number
    WHERE 
        prs.steam_account_id = ${steam_id}
    GROUP BY 
        m.match_id, m.map_name, r.round_number, r.winning_team
)
-- Economic strategy effectiveness analysis
SELECT 
    economic_state,
    COUNT(DISTINCT (match_id, round_number)) AS rounds_played,
    ROUND(COUNT(DISTINCT CASE WHEN winning_team = 'CT' OR winning_team = 'T' THEN (match_id, round_number) END)::numeric / 
        NULLIF(COUNT(DISTINCT (match_id, round_number)), 0) * 100, 2) AS win_rate,
    ROUND(SUM(total_kills)::numeric / NULLIF(COUNT(DISTINCT (match_id, round_number)), 0), 2) AS avg_kills_per_round,
    ROUND(AVG(avg_equip_value), 2) AS avg_investment,
    ROUND(AVG(avg_remaining_money), 2) AS avg_remaining_money,
    ROUND(SUM(total_kills)::numeric / NULLIF(SUM(avg_equip_value) / 1000, 0), 2) AS kills_per_1000_spent
FROM 
    round_economics
GROUP BY 
    economic_state
ORDER BY 
    win_rate DESC;
-- Economic transitions analysis: impact of previous round's economy
WITH round_sequence AS (
    SELECT DISTINCT ON (prs1.match_id, prs1.round_number)
        prs1.match_id,
        prs1.round_number,
        prs1.steam_account_id,
        prs1.equip_value AS current_equip_value,
        prs1.money AS current_money,
        prs1.round_kills,
        prs1.team,
        r1.winning_team = prs1.team AS round_won,
        LAG(prs1.equip_value) OVER (
            PARTITION BY prs1.match_id
            ORDER BY prs1.round_number
        ) AS prev_equip_value,
        LAG(r1.winning_team = prs1.team) OVER (
            PARTITION BY prs1.match_id
            ORDER BY prs1.round_number
        ) AS prev_round_won
    FROM 
        stats_playerroundstate prs1
    JOIN 
        matches_round r1 ON 
            prs1.match_id = r1.match_id AND 
            prs1.round_number = r1.round_number
    WHERE 
        prs1.steam_account_id = ${steam_id}
)
SELECT 
    CASE 
        WHEN prev_equip_value < 1000 THEN 'After Eco (< $1000)'
        WHEN prev_equip_value BETWEEN 1000 AND 2500 THEN 'After Semi-Eco ($1000-$2500)'
        WHEN prev_equip_value BETWEEN 2501 AND 4000 THEN 'After Semi-Buy ($2501-$4000)'
        WHEN prev_equip_value > 4000 THEN 'After Full Buy (> $4000)'
        ELSE 'First Round'
    END AS previous_economic_state,
    CASE 
        WHEN prev_round_won IS TRUE THEN 'Won'
        WHEN prev_round_won IS FALSE THEN 'Lost'
        ELSE 'First Round'
    END AS previous_round_outcome,
    COUNT(*) AS rounds_played,
    ROUND(AVG(current_equip_value), 2) AS avg_current_investment,
    ROUND(AVG(current_money), 2) AS avg_current_money,
    ROUND(AVG(round_kills), 2) AS avg_kills,
    ROUND(COUNT(CASE WHEN round_won THEN 1 END)::numeric / 
          NULLIF(COUNT(*), 0) * 100, 2) AS win_percentage
FROM 
    round_sequence
WHERE 
    round_number > 1  -- Exclude first round since it doesn't have previous state
GROUP BY 
    previous_economic_state, previous_round_outcome
HAVING 
    COUNT(*) >= 3  -- Only include scenarios with at least 3 occurrences
ORDER BY 
    previous_economic_state, previous_round_outcome;

Our advanced analytics functions are implemented through a combination of complex SQL queries and Python-based post-processing. The weapon_analyzer.py module, for instance, uses a sophisticated temporal relationship algorithm to accurately attribute kills to specific weapons based on the sequence of state changes:

def get_weapon_analysis(steam_id: str) -> List[Dict[Any, Any]]:
    try:
        with connection.cursor() as cursor:
            cursor.execute("""
                -- ADVANCED WEAPON ANALYSIS QUERY
                -- This query performs temporal sequence analysis to attribute kills
                -- to the correct weapons based on which weapon was active when kills occurred.
                WITH PlayerStateChanges AS (
                    -- STEP 1: Identify points where round_kills increases
                    -- This CTE analyzes the PlayerRoundState table to find moments when
                    -- a player's kill count increases, indicating they got a kill.
                    -- The LAG window function compares the current round_kills with the
                    -- previous round_kills value (ordered by timestamp) to calculate the
                    -- kill_increase (how many kills were just added).
                    SELECT 
                        prs1.match_id, 
                        prs1.round_number,
                        prs1.state_timestamp,
                        -- Calculate kill increase using LAG window function
                        -- This finds the exact moments when kills increased
                        prs1.round_kills - LAG(prs1.round_kills, 1, 0) OVER (
                            PARTITION BY prs1.match_id, prs1.round_number 
                            ORDER BY prs1.state_timestamp
                        ) AS kill_increase,
                        prs1.money
                    FROM stats_playerroundstate prs1
                    WHERE 
                        prs1.steam_account_id = %s
                        AND prs1.round_kills > 0  -- Only include states with kills
                ),
                ActiveWeapons AS (
                    -- STEP 2: Identify which weapon was active at each point in time
                    -- This CTE tracks which weapons were 'active' (being used) at which times.
                    -- We use ROW_NUMBER to identify unique weapon activations, avoiding
                    -- counting every state update as a separate usage instance.
                    SELECT
                        pw.match_id,
                        pw.round_number,
                        pw.weapon_id,
                        pw.state_timestamp,
                        w.name AS weapon_name,
                        w.type AS weapon_type,
                        -- Rank each weapon state by timestamp within round
                        -- This helps identify distinct weapon activations
                        ROW_NUMBER() OVER (
                            PARTITION BY pw.match_id, pw.round_number, pw.weapon_id 
                            ORDER BY pw.state_timestamp
                        ) AS activation_rank
                    FROM stats_playerweapon pw
                    JOIN stats_weapon w ON pw.weapon_id = w.weapon_id
                    WHERE 
                        pw.steam_account_id = %s
                        AND pw.state = 'active'  -- Only include active weapons
                        -- Exclude non-weapon items like grenades, C4, etc.
                        AND w.type NOT IN ('Grenade', 'C4')
                        AND w.type IS NOT NULL
                        AND w.type != 'StackableItem'
                ),
                WeaponKills AS (
                    -- STEP 3: Join kills with active weapons to attribute kills correctly
                    -- This CTE correlates kills with active weapons based on timestamps.
                    -- We join PlayerStateChanges with ActiveWeapons where:
                    -- 1. They're in the same match and round
                    -- 2. The kill timestamp is >= the weapon activation timestamp
                    -- 3. There was an actual kill increase
                    -- This temporal relationship analysis is the key to attributing
                    -- kills to the correct weapons.
                    SELECT
                        aw.weapon_id,
                        aw.weapon_name,
                        aw.weapon_type,
                        -- Count distinct rounds where this weapon was used
                        COUNT(DISTINCT CONCAT(aw.match_id, '-', aw.round_number)) AS rounds_used,
                        -- Count total state records (approximates usage frequency)
                        COUNT(aw.match_id) AS state_count,
                        -- Sum kill increases that occurred while this weapon was active
                        SUM(CASE WHEN psc.kill_increase > 0 THEN psc.kill_increase ELSE 0 END) AS total_kills,
                        -- Track money for economic analysis
                        SUM(psc.money) AS total_money
                    FROM ActiveWeapons aw
                    LEFT JOIN PlayerStateChanges psc ON 
                        -- Join on match and round
                        aw.match_id = psc.match_id 
                        AND aw.round_number = psc.round_number
                        -- The kill happened AFTER this weapon became active
                        AND psc.state_timestamp >= aw.state_timestamp
                        -- Only include actual kill increases
                        AND (psc.kill_increase > 0)
                    GROUP BY aw.weapon_id, aw.weapon_name, aw.weapon_type
                )
                -- STEP 4: Final output with calculated metrics
                -- This final query calculates performance metrics like:
                -- - Average kills per round
                -- - Average money during weapon usage
                -- - Overall weapon effectiveness
                SELECT
                    weapon_id,
                    weapon_name,
                    weapon_type,
                    rounds_used,
                    state_count,
                    total_kills,
                    -- Calculate average kills per round
                    CASE WHEN rounds_used > 0 THEN total_kills::float / rounds_used ELSE 0 END AS avg_kills,
                    -- Calculate average money during weapon usage
                    CASE WHEN state_count > 0 THEN total_money::float / state_count ELSE 0 END AS avg_money
                FROM WeaponKills
                ORDER BY state_count DESC;  -- Sort by usage frequency
            """, [steam_id, steam_id])
            # process the query results into a list of dictionaries
            # this makes the data easy to work with in django views
            weapon_analysis = []
            for row in cursor.fetchall():
                weapon_analysis.append({
                    'weapon_id': row[0],
                    'weapon__name': row[1],
                    'weapon__type': row[2],
                    'rounds_used': row[3],
                    'times_active': row[4],
                    'total_kills': row[5],
                    'avg_kills_when_active': row[6],
                    'avg_money': row[7]
                })
            return weapon_analysis
    except Exception as e:
        logger.error(f"Error in weapon analysis for {steam_id}: {str(e)}")
        return []

This advanced functionality sets CounterTrak apart from simpler statistical tracking systems, providing players with actionable insights rather than just raw numbers. The Performance Pattern Recognition engine identifies strengths, weaknesses, and optimal strategies based on each player's unique gameplay patterns.

Implementation Details

CounterTrak employs a multi-tier, event-driven architecture designed to efficiently process game state data in real-time while maintaining data consistency and system responsiveness. The architecture consists of several key components, each with specific responsibilities in the data flow pipeline. The high-level architecture is illustrated in the diagram below: Image

At the top level, the Asynchronous GSI Server component (implemented in backend/gsi/async_server.py) receives game state payloads from multiple CS2 clients simultaneously. This component uses Python's asyncio framework to provide non-blocking I/O, allowing it to handle many concurrent connections efficiently without creating a thread per connection. The server authenticates incoming payloads using token validation and routes them to the appropriate match processor.

The Match/Player Manager component (backend/gsi/match_manager.py) serves as a central coordination point, tracking active matches and creating new match processors as needed. This component maintains a dictionary of active match processors keyed by match ID, ensuring that each match's data remains isolated and consistent. The manager is primarily responsible for routing payloads to their appropriate MatchProcessors, but does perform some basic preliminary validation to ensure that the data hails from a new or in-progress match.

Individual Match Processors (backend/gsi/match_processor.py) handle the game state for specific matches, maintaining state between updates and managing the temporal sequence of events. Each processor operates independently, parsing game events using a delegate object called PayloadExtractor, tracking round transitions, and persisting data at appropriate points (typically at round boundaries). This isolation ensures that data from one match cannot interfere with another, while also enabling parallel processing of multiple matches.

At the bottom tier, the Async Database Connection Pool provides efficient, non-blocking database access. This component manages connections to the PostgreSQL database, using asyncio-compatible database drivers to perform database operations without blocking the event loop. All INSERT and UPDATE operations are handled in gsi/backend/django_integration.py, with calls being made from any instantiated MatchProcessors. The connection pool ensures that database operations are properly sequenced and transactionally consistent.

This architecture provides several key advantages:

  1. Scalability: The system can handle many concurrent matches without proportional resource consumption, as the async I/O model is much more efficient than traditional threading.
  2. Fault Isolation: Problems in one match processor don't affect others, improving system resilience.
  3. Data Consistency: By processing each match independently and using appropriate transaction boundaries (typically round completion), the system ensures data is consistently persisted.
  4. Performance: The non-blocking I/O model keeps the system responsive even under heavy load, as it can process other requests while waiting for I/O operations to complete.

The architecture also includes a separate Django Web Application that provides the user interface and REST API for accessing match statistics and analysis results. This component operates independently from the GSI processing pipeline, accessing the same database but focusing on query operations rather than data ingestion.

Technology Stack

The CounterTrak application is built on a modern, scalable technology stack designed to efficiently handle real-time game data processing, storage, and analysis:

  1. Database Layer:
    • PostgreSQL 14: Primary RDBMS providing robust data storage, complex querying capabilities, and transaction support.
    • Database Schema: Carefully normalized to BCNF with optimized indexes for query performance.
    • Connection Pooling: Implemented through Django's connection pool for efficient database access.
  2. Backend Processing:
    • Python 3.10+: Core programming language for backend development.
    • Django 5.2: Web framework providing ORM, authentication, and routing capabilities.
    • Django REST Framework 3.16: For building the REST API endpoints.
    • asyncio: Python's asynchronous I/O framework, used for non-blocking operations.
    • aiohttp 3.11+: Asynchronous HTTP client/server for Python, used for the GSI server.
  3. Frontend:
    • HTML5/CSS3/JavaScript: Standard web technologies for UI development.
    • Bootstrap 5.3: Frontend CSS framework for responsive design.
  4. Development & DevOps:
    • Git: Version control system for code management.
    • GitHub: Repository hosting.
    • dotenv: Environment variable management for secure configuration.
    • pexpect: Used for automation in development scripts.

Our technology choices reflect a focus on scalability, maintainability, and performance. The use of asynchronous processing with asyncio and aiohttp is particularly notable, as it allows our system to handle many concurrent game clients efficiently without the overhead of traditional threading models.

Data Flow and Processing Pipeline

The CounterTrak data flow pipeline is designed to efficiently process game state information from raw GSI payloads to structured database records and ultimately to meaningful analytical insights. This pipeline consists of several key stages:

  1. Payload Reception and Authentication:
    • CS2 clients send HTTP POST requests containing GSI payloads to the async server.
    • The server authenticates payloads using the auth token from the request.
    • This stage is implemented in backend/gsi/async_server.py.
  2. Payload Routing and Match Association:
    • The match manager extracts the match identifier from the payload.
    • If this is a new match, a new match processor is created.
    • The payload is routed to the appropriate match processor.
    • This stage is implemented in backend/gsi/match_manager.py.
  3. Game State Extraction and Normalization:
    • The PayloadExtractor class (in backend/gsi/payloadextractor.py) parses the raw JSON payload.
    • The extractor creates structured data objects for match state, player state, round state, and weapon states.
    • These structured objects map to database tables in the relational schema, an example for the Match table is seen below:
def extract_match_state(self, payload: Dict, timestamp: int) -> Optional[MatchState]:
    if 'map' not in payload or 'provider' not in payload:
        return None
    map_data = payload['map']
    provider_data = payload['provider']
    # use centralized utility function to get base_match_id
    base_match_id = extract_base_match_id(payload)
    if not base_match_id:
        return None
    # get raw round number from map payload portion
    raw_round = map_data.get('round', 0)
    # add 1 to convert from zero-indexed to one-indexed
    adjusted_round = raw_round + 1
    # return full match state with the provided timestamp
    return MatchState(
        match_id=base_match_id,
        mode=map_data.get('mode', 'casual'),
        map_name=map_data.get('name', 'unknown_map'),
        phase=map_data.get('phase', 'unknown'),
        round=adjusted_round,
        team_ct_score=map_data.get('team_ct', {}).get('score', 0),
        team_t_score=map_data.get('team_t', {}).get('score', 0),
        timestamp=timestamp
    )

The multi-tier architecture and data flow pipeline of CounterTrak demonstrate a sophisticated approach to real-time game data processing. By implementing asynchronous I/O throughout the system, we've created a solution that efficiently handles the high-volume, bursty nature of GSI data while maintaining responsiveness. The well-defined responsibility boundaries between components (GSI server, match processors, database layer) provide clear separation of concerns, making the system both maintainable and extensible. This architecture not only meets the requirements of our current implementation but also establishes a solid foundation for future enhancements, such as more advanced analytics algorithms or integration with additional data sources.

Experiences and Challenges

Technical Challenges

Throughout the development of CounterTrak, our team encountered and overcame several significant technical challenges:

  1. Multiprocess Dual-Server Architecture: Given our choice of PostgreSQL with the Django backend framework, we were forced to reconcile with how we would integrate our original Game State Integration server with proper database persistence. We realized that the GSI Server needed to listen for incoming payloads within a main loop (python async_server.py), and Django required its own main loop (python manage.py runserver 0.0.0.0:8000) for communication with the PSQL instance—both were blocking processes. To overcome this, we decided to implement a dual-server approach in backend/run_servers.py, which compartmentalized each server's main loop within its own subprocess. To consolidate the disparate output streams to a common destination, we maintained two separate threads for the servers, each of which piped their output to standard output via the method stream_output(process, prefix). Each call then iteratively logged the output from the two threads using for line in iter(process.stdout.readline):, if line:, print(f"[{prefix}] {line}", end="", flush=True), with proper try-except handling for IOError(s) and ValueError(s). Additionally, we implemented intelligent thread and subprocess cleanup, to ensure that no child threads or subprocesses were left hanging after server shutdown.
  2. Asynchronous Processing Complexity: Implementing a fully asynchronous system using Python's asyncio library required a substantial paradigm shift in our programming approach. We needed to carefully manage concurrency, especially in the match processor components where race conditions could lead to data inconsistencies. This was addressed through strategic use of locks (asyncio.Lock) and careful transaction management.
  3. Temporal Sequence Analysis: One of our most complex challenges was implementing accurate temporal relationship analysis for weapon effectiveness. Unlike simpler statistics systems that only count kills, we needed to determine which weapon was active at the exact moment a kill occurred. This required sophisticated SQL queries using window functions and temporal joins:
WITH PlayerStateChanges AS (
    SELECT 
        prs1.match_id, 
        prs1.round_number,
        prs1.state_timestamp,
        prs1.round_kills - LAG(prs1.round_kills, 1, 0) OVER (
            PARTITION BY prs1.match_id, prs1.round_number 
            ORDER BY prs1.state_timestamp
        ) AS kill_increase
    FROM stats_playerroundstate prs1
    WHERE 
        prs1.steam_account_id = %s
        AND prs1.round_kills > 0
),
ActiveWeapons AS (
    -- Track active weapons at each timestamp
    -- ...
)
  1. Database Performance Optimization: Managing the potentially high volume of GSI payloads required careful database design. We implemented several optimizations:
    • Strategic denormalization for frequently accessed data
    • Composite indexes for common query patterns
    • Batch processing of state updates to reduce database load
  2. Authentication and Security: Ensuring that only authorized clients could submit game state data required a token-based authentication system. We implemented a token cache with periodic refreshing to maintain security while minimizing database queries.

Lessons Learned

The CounterTrak project provided numerous valuable insights that have enhanced our understanding of both technical and project management aspects:

  1. Asynchronous Design Benefits: The decision to use an asynchronous architecture with Python's asyncio framework proved extremely beneficial. It allowed our system to handle many concurrent connections with minimal resource consumption, making the application scalable without proportional resource requirements. This approach is significantly more efficient than traditional multi-threading for I/O-bound operations like our GSI server.
  2. Importance of Domain Modeling: Starting with a carefully designed entity-relationship model before implementing database schemas was crucial. The time invested in proper domain modeling paid dividends throughout the project by providing a clear conceptual framework that guided implementation decisions.
  3. Data Structure Separation: Maintaining clear separation between raw payload data structures and database models prevented data format changes in the GSI system from directly affecting our database schema. This abstraction layer, implemented through our PayloadExtractor class, provided important flexibility.
  4. Progressive Enhancement: Building the system incrementally - first focusing on correct data capture, then basic statistics, and finally advanced analytics - allowed us to deliver value at each stage while continuing to enhance functionality.
  5. Centralized Logging: Implementing a comprehensive logging system early in development proved invaluable for debugging complex asynchronous behaviors and monitoring system performance.

Future Extensions

Looking forward, we envision several promising directions for extending CounterTrak:

  1. Advanced Pattern Recognition: The current Performance Pattern Recognition engine could be enhanced with machine learning algorithms (using pytorch or tensorflow) to identify more complex patterns in player behavior and provide more personalized recommendations.
  2. Comparative Analytics: Implementing functionality to compare player performance against population averages or professional player benchmarks would provide additional context for performance evaluation.
  3. Real-time Feedback System: Developing a system to provide real-time feedback during matches could help players adjust their strategies on-the-fly based on historical performance data.
  4. API Expansion: Creating a more comprehensive API would enable third-party developers to build additional tools and visualizations using CounterTrak's data and analytics engine.

References

  1. Django Project. (2025). Django Documentation. https://docs.djangoproject.com/en/5.2/
  2. Python Software Foundation. (2025). asyncio — Asynchronous I/O. https://docs.python.org/3.10/library/asyncio.html
  3. aiohttp. (2025). aiohttp Documentation. https://docs.aiohttp.org/en/stable/
  4. Valve Corporation. (2024). Counter-Strike: Global Offensive Game State Integration. https://developer.valvesoftware.com/wiki/Counter-Strike:_Global_Offensive_Game_State_Integration
  5. Plotnikov, A. (2024). Development of an esports HUD for Counter-Strike 2. Bachelor's thesis, Esports Business.
  6. PostgreSQL Global Development Group. (2025). PostgreSQL Documentation. https://www.postgresql.org/docs/14/
  7. The Python Package Index (PyPI). (2025). colorlog 6.9.0. https://pypi.org/project/colorlog/
  8. Django REST Framework. (2025). Django REST Framework Documentation. https://www.django-rest-framework.org/

About

A fully-functional and lightweight Counter Strike 2 statistics tracker web-application

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors