2026-09-05 21:10:18 +00:00
import asyncio
import random
import uuid
from typing import Dict , List , Optional , Tuple
from app . config import settings
2026-09-05 21:20:39 +00:00
from app . models import (
BoardConfig ,
BoardState ,
MoveCheckResult ,
MoveResponse ,
Player ,
PlayerCreate ,
TurnInfo ,
DIRECTION_OFFSETS ,
AvailableMovesResponse ,
)
STANDARD_DIRECTIONS = [
( " UP " , 0 , - 1 ) ,
( " UP_RIGHT " , 1 , - 1 ) ,
( " RIGHT " , 1 , 0 ) ,
( " DOWN_RIGHT " , 1 , 1 ) ,
( " DOWN " , 0 , 1 ) ,
( " DOWN_LEFT " , - 1 , 1 ) ,
( " LEFT " , - 1 , 0 ) ,
( " UP_LEFT " , - 1 , - 1 ) ,
]
2026-09-05 21:10:18 +00:00
class GameEngine :
def __init__ ( self ) :
self . _lock = asyncio . Lock ( )
self . players : Dict [ str , Player ] = { }
2026-09-05 21:20:39 +00:00
self . turn_order : List [ str ] = [ ]
self . current_turn_index : int = 0
self . round_number : int = 1
self . turn_number : int = 0
2026-09-05 21:10:18 +00:00
self . config = BoardConfig (
min_x = settings . GRID_MIN_X ,
max_x = settings . GRID_MAX_X ,
min_y = settings . GRID_MIN_Y ,
max_y = settings . GRID_MAX_Y ,
grid_cells_x = settings . GRID_MAX_X - settings . GRID_MIN_X ,
grid_cells_y = settings . GRID_MAX_Y - settings . GRID_MIN_Y ,
)
2026-09-05 21:20:39 +00:00
def _get_occupied_coordinates ( self ) - > Dict [ Tuple [ int , int ] , Player ] :
return { ( p . x , p . y ) : p for p in self . players . values ( ) }
2026-09-05 21:10:18 +00:00
def _find_random_free_position ( self ) - > Tuple [ int , int ] :
occupied = self . _get_occupied_coordinates ( )
total_possible = ( self . config . max_x - self . config . min_x + 1 ) * (
self . config . max_y - self . config . min_y + 1
)
if len ( occupied ) > = total_possible :
return (
random . randint ( self . config . min_x , self . config . max_x ) ,
random . randint ( self . config . min_y , self . config . max_y ) ,
)
for _ in range ( 100 ) :
rx = random . randint ( self . config . min_x , self . config . max_x )
ry = random . randint ( self . config . min_y , self . config . max_y )
if ( rx , ry ) not in occupied :
return ( rx , ry )
all_coords = [
( x , y )
for x in range ( self . config . min_x , self . config . max_x + 1 )
for y in range ( self . config . min_y , self . config . max_y + 1 )
if ( x , y ) not in occupied
]
return random . choice ( all_coords )
2026-09-05 21:20:39 +00:00
def _get_current_player ( self ) - > Optional [ Player ] :
if not self . turn_order :
return None
safe_index = self . current_turn_index % len ( self . turn_order )
player_id = self . turn_order [ safe_index ]
return self . players . get ( player_id )
def _get_turn_info ( self ) - > TurnInfo :
current = self . _get_current_player ( )
return TurnInfo (
current_player_id = current . id if current else None ,
current_player_name = current . name if current else None ,
round_number = self . round_number ,
turn_number = self . turn_number ,
turn_order = list ( self . turn_order ) ,
)
def _advance_turn ( self ) :
if not self . turn_order :
self . current_turn_index = 0
return
self . turn_number + = 1
self . current_turn_index = ( self . current_turn_index + 1 ) % len ( self . turn_order )
if self . current_turn_index == 0 :
self . round_number + = 1
2026-09-05 21:10:18 +00:00
async def register_player ( self , player_in : PlayerCreate ) - > Player :
async with self . _lock :
player_id = f " bot_ { uuid . uuid4 ( ) . hex [ : 8 ] } "
x , y = self . _find_random_free_position ( )
2026-09-05 21:20:39 +00:00
2026-09-05 21:10:18 +00:00
player = Player (
id = player_id ,
name = player_in . name ,
color = player_in . color ,
x = x ,
y = y ,
)
self . players [ player_id ] = player
2026-09-05 21:20:39 +00:00
self . turn_order . append ( player_id )
2026-09-05 21:10:18 +00:00
return player
async def get_player ( self , player_id : str ) - > Optional [ Player ] :
async with self . _lock :
return self . players . get ( player_id )
async def get_all_players ( self ) - > List [ Player ] :
async with self . _lock :
return list ( self . players . values ( ) )
async def remove_player ( self , player_id : str ) - > bool :
async with self . _lock :
2026-09-05 21:20:39 +00:00
if player_id not in self . players :
return False
del self . players [ player_id ]
if player_id in self . turn_order :
idx = self . turn_order . index ( player_id )
self . turn_order . remove ( player_id )
if self . turn_order :
if idx < self . current_turn_index :
self . current_turn_index - = 1
self . current_turn_index % = len ( self . turn_order )
else :
self . current_turn_index = 0
return True
2026-09-05 21:10:18 +00:00
async def reset ( self ) - > None :
async with self . _lock :
self . players . clear ( )
2026-09-05 21:20:39 +00:00
self . turn_order . clear ( )
self . current_turn_index = 0
self . round_number = 1
self . turn_number = 0
2026-09-05 21:10:18 +00:00
async def get_board_state ( self ) - > BoardState :
async with self . _lock :
players_list = list ( self . players . values ( ) )
return BoardState (
config = self . config ,
player_count = len ( players_list ) ,
players = players_list ,
2026-09-05 21:20:39 +00:00
turn = self . _get_turn_info ( ) ,
2026-09-05 21:10:18 +00:00
)
2026-09-05 21:20:39 +00:00
def _check_move_internal (
self ,
player : Player ,
dx : int ,
dy : int ,
direction_name : str ,
occupied_map : Dict [ Tuple [ int , int ] , Player ] ,
) - > MoveCheckResult :
target_x = player . x + dx
target_y = player . y + dy
# 1. Boundary Wall Check
if (
target_x < self . config . min_x
or target_x > self . config . max_x
or target_y < self . config . min_y
or target_y > self . config . max_y
) :
return MoveCheckResult (
direction = direction_name ,
dx = dx ,
dy = dy ,
target_x = target_x ,
target_y = target_y ,
available = False ,
reason = f " Wall collision at ( { target_x } , { target_y } ). Grid boundaries are [ { self . config . min_x } .. { self . config . max_x } , { self . config . min_y } .. { self . config . max_y } ]. " ,
)
# 2. Occupancy Check
occupant = occupied_map . get ( ( target_x , target_y ) )
if occupant is not None and occupant . id != player . id :
return MoveCheckResult (
direction = direction_name ,
dx = dx ,
dy = dy ,
target_x = target_x ,
target_y = target_y ,
available = False ,
reason = f " Space occupied by { occupant . name } ( { occupant . id } ) at ( { target_x } , { target_y } ). " ,
)
return MoveCheckResult (
direction = direction_name ,
dx = dx ,
dy = dy ,
target_x = target_x ,
target_y = target_y ,
available = True ,
reason = None ,
)
async def check_single_move ( self , player_id : str , direction_input : str ) - > MoveCheckResult :
async with self . _lock :
player = self . players . get ( player_id )
if not player :
raise KeyError ( f " Player ' { player_id } ' not found " )
normalized = direction_input . strip ( ) . upper ( ) . replace ( " " , " _ " )
if normalized not in DIRECTION_OFFSETS :
raise ValueError (
f " Unknown direction ' { direction_input } ' . Allowed: UP, DOWN, LEFT, RIGHT, UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT (or N, S, W, E, NW, NE, SW, SE) "
)
dx , dy = DIRECTION_OFFSETS [ normalized ]
occupied = self . _get_occupied_coordinates ( )
return self . _check_move_internal ( player , dx , dy , normalized , occupied )
async def get_available_moves ( self , player_id : str ) - > AvailableMovesResponse :
async with self . _lock :
player = self . players . get ( player_id )
if not player :
raise KeyError ( f " Player ' { player_id } ' not found " )
current_turn_player = self . _get_current_player ( )
is_turn = current_turn_player is not None and current_turn_player . id == player_id
occupied = self . _get_occupied_coordinates ( )
moves : Dict [ str , MoveCheckResult ] = { }
for name , dx , dy in STANDARD_DIRECTIONS :
moves [ name ] = self . _check_move_internal ( player , dx , dy , name , occupied )
return AvailableMovesResponse (
player_id = player . id ,
player_name = player . name ,
current_x = player . x ,
current_y = player . y ,
is_turn = is_turn ,
current_turn_player_id = current_turn_player . id if current_turn_player else None ,
moves = moves ,
)
async def move_player ( self , player_id : str , dx : int , dy : int , direction_name : str ) - > MoveResponse :
async with self . _lock :
player = self . players . get ( player_id )
if not player :
raise KeyError ( f " Player ' { player_id } ' not found " )
current_turn_player = self . _get_current_player ( )
if not current_turn_player or current_turn_player . id != player_id :
curr_name = current_turn_player . name if current_turn_player else " Nobody "
curr_id = current_turn_player . id if current_turn_player else " None "
raise PermissionError (
f " It is not your turn. Current turn belongs to ' { curr_name } ' ( { curr_id } ). "
)
occupied = self . _get_occupied_coordinates ( )
check = self . _check_move_internal ( player , dx , dy , direction_name , occupied )
if not check . available :
raise ValueError ( f " Illegal move: { check . reason } " )
prev_pos = { " x " : player . x , " y " : player . y }
player . x = check . target_x
player . y = check . target_y
new_pos = { " x " : player . x , " y " : player . y }
# Advance turn order to next player
self . _advance_turn ( )
turn_info = self . _get_turn_info ( )
return MoveResponse (
success = True ,
player = player ,
direction = direction_name ,
previous_position = prev_pos ,
new_position = new_pos ,
turn = turn_info ,
)
async def pass_turn ( self , player_id : str ) - > TurnInfo :
async with self . _lock :
player = self . players . get ( player_id )
if not player :
raise KeyError ( f " Player ' { player_id } ' not found " )
current_turn_player = self . _get_current_player ( )
if not current_turn_player or current_turn_player . id != player_id :
curr_name = current_turn_player . name if current_turn_player else " Nobody "
curr_id = current_turn_player . id if current_turn_player else " None "
raise PermissionError (
f " Cannot pass: it is not your turn. Current turn belongs to ' { curr_name } ' ( { curr_id } ). "
)
self . _advance_turn ( )
return self . _get_turn_info ( )
2026-09-05 21:10:18 +00:00
# Global game engine instance
game_engine = GameEngine ( )