Guard

SPGG Bot Docs

Give me your ID if you want to cross this world!

#Undisputed Bot Reference

File: und.py · Lines: 1456 · Game: Undisputed Boxing

#Overview

The Undisputed bot is the most complex bot, featuring:

  • OCR-based fighter selection using Tesseract
  • Fuzzy text matching using RapidFuzz
  • Screen template matching for state detection
  • Pixel-based damage stats reading during round breaks
  • Color-based winner detection after fights
  • Auto-focus loop running every 1 second

#Global Variables

Variable Type Description
current_red str Red corner fighter name
current_blue str Blue corner fighter name
weight str Weight class (1-10)
match_winner str|None Winner: 'player1', 'player2', or 'DRAW'
last_queue_id int|None Server-provided match ID
match_score_sent bool Whether match-score was emitted this match
awaiting_score_emit bool Gate: block start until score is sent
stats_captured bool Whether break-time stats were captured
screen_comparator ScreenComparator Template matching instance

#Functions

Print to console + emit to /logs namespace.


#log_to_file(message) ACTIVE

Write to und.log if USE_LOG_FILE=True, else print.


#focus_window_by_exact_title(title, quiet) ACTIVE

Focus game window via pywinauto. Connects to Undisputed.exe.

Parameter Type Default Description
title str Window title
quiet bool False Suppress log output

#_auto_focus_loop() ACTIVE

Background thread that calls focus_window_by_exact_title('Undisputed', quiet=True) every 1 second.


#_start_auto_focus_if_needed() ACTIVE

Start the auto-focus thread if not already running.


#OCR Functions

#read_first_name(frame, away) ACTIVE

OCR read fighter's first name from screen.

Parameter Type Default Description
frame np.ndarray Screenshot as numpy array
away bool False If True, read from away corner (x offset +1230)

ROI Coordinates: (231, 863) to (455, 890) for home


#read_last_name(frame, away) ACTIVE

OCR read fighter's last name.

ROI: (221, 890) to (530, 933) for home, (1366, 890) to (1706, 930) for away


#read_player_name(frame, away) ACTIVE

OCR read fighter's full player name.

ROI: (206, 941) to (485, 968) for home, (1424, 946) to (1728, 971) for away


#read_p1_stuck(frame) ACTIVE

OCR check if game is stuck at P1 selection screen.

ROI: (1077, 470) to (1202, 573)


#compare_text_fuzzy(text1, text2, threshold) ACTIVE

Compare two texts using multiple RapidFuzz algorithms and return best score.

Parameter Type Default Description
text1 str First text
text2 str Second text
threshold int 80 Minimum match score

Algorithms used: fuzz.ratio, fuzz.partial_ratio, fuzz.token_sort_ratio, fuzz.WRatio

Returns: (is_match: bool, best_score: float)


#Fight Setup

#startMatch1() ACTIVE

Complete fighter selection flow:

  1. Weight Selection: Maps weight class (1-10) to number of Key.2 presses
  2. Red Corner Selection: Loop OCR + fuzzy match → navigate with Key.right
  3. Blue Corner Selection: Same process for away corner

Special OCR Corrections:

  • JOE LOUIS → matches against 'LoYIg' fallback
  • REGIS PROGAIS → matches against 'PROGCRAIS'
  • SAUL ALVAREZ → matches against 'SAGL' or 'SAOL'
  • ERIC ESCH → matches against 'BRIG'
  • MUHAMMAD ALI → excludes '64' variant

#_start_sequence_from_payload(data, source_label) ACTIVE

Unified start sequence handler (used by both on_new_match and on_start_match).

Flow:

  1. Clear flags → Stop memory thread
  2. Start OBS stream (with retry up to 5x)
  3. Parse payload (player names, weight, match ID)
  4. Navigate game menus (15s wait → Press F → Press R)
  5. P1 stuck detection
  6. Call startMatch1() for fighter selection
  7. Set difficulty → Confirm fight
  8. Start memory_listener_thread()

#Screen State Handlers

#on_screen_changed(screen_name, similarity) ACTIVE

Main screen state callback. Handles:

Screen Action
'start' Reset match_is_started flag
'winniewinner' Trigger winner detection flow
'none' Reset team names

#Winner Detection

#detect_color_name(x, y) ACTIVE

Detect winner side by pixel color at coordinates.

Color (Hex) Side
#8d0000 (Red) player1
#0d60c1 (Blue) player2
#2a2a2a (Grey) DRAW

Uses Euclidean color distance for matching.


#get_pixel_color(x, y) INTERNAL

Get hex color of pixel at screen coordinates.

#hex_to_rgb(hex_color) INTERNAL

Convert hex color to RGB tuple.

#color_distance(c1, c2) INTERNAL

Calculate Euclidean distance between two RGB colors.


#Damage Stats (Break Time)

#get_bar_percent(start_x, end_x, y_pos, avoid_hex, tolerance) ACTIVE

Scan pixel bar to calculate fill percentage.

  • Home bars: scan left-to-right
  • Away bars: scan right-to-left
  • Skips pixels matching background color (#010101)

#get_box_percent_inverse(points, avoid_hex, tolerance) ACTIVE

Calculate percentage of active (non-black) pixels from a set of coordinate points.


#get_all_screen_stats() ACTIVE

Read all damage stats from screen:

Stat Home Coords Away Coords
Head 170→251 @ y=651 1750→1670 @ y=651
Body 170→251 @ y=689 1750→1670 @ y=689
L Swell 170→251 @ y=798 1750→1670 @ y=798
R Swell 170→251 @ y=835 1750→1670 @ y=835
L Cuts Box at 4 points @726 Box at 4 points @726
R Cuts Box at 4 points @762 Box at 4 points @762

#log_stats_to_file(data) ACTIVE

Append stats to match_stats_log.json (JSON Lines format) for debugging.


#Memory Thread

#memory_listener_thread() ACTIVE

Main loop running every 0.5 seconds:

  1. Read gameTime and currentRound
  2. During break time (gameTime == 0): capture screen stats
  3. Read boxer scorecard data from memory
  4. Detect match start when gameTime > 0
  5. Emit match-data with round scores

#Socket Events (Emitted)

Event Payload When
waiting {uuid, minutes} Bot ready
myid {id: uuid} Device identification
start-match {uuid, game_time} Match started
home-away {uuid, player_1, player_2} Fighter names
team-full {uuid, player_1, player_2} Fighter data
match-data Score/round data Every 0.5s
round-break-stats {uuid, round, stats} During round break
winner-detected {uuid, winner, total_rounds, game_time, lastQueueId} Winner screen detected
match-score Full scorecard data After winner detection

#Socket Events (Received)

Event Action
new-match Start new match via _start_sequence_from_payload()
starting-match Same as new-match
pong Log pong response