Source code for haive.games.single_player.flow_free.state
"""State model for Flow Free game.This module defines the game state for Flow Free, tracking the board, flows, and gameprogress."""fromuuidimportuuid4frompydanticimportBaseModel,Field,computed_fieldfromhaive.games.single_player.baseimportSinglePlayerGameStatefromhaive.games.single_player.flow_free.modelsimportPipeDirection,Position
[docs]classFlowEndpoint(BaseModel):"""An endpoint (colored dot) in Flow Free. Attributes: position: Position of the endpoint on the board. is_start: Whether this is the start endpoint (otherwise it's the end). """position:Positionis_start:bool=True
[docs]classFlow(BaseModel):"""A flow in Flow Free, consisting of two endpoints and a path of pipes. Attributes: id: Unique identifier for the flow. color: Color of the flow. start: Starting endpoint. end: Ending endpoint. path: List of positions forming the path between endpoints. completed: Whether the flow is complete (endpoints connected). """id:str=Field(default_factory=lambda:str(uuid4()))color:strstart:FlowEndpointend:FlowEndpointpath:list[Position]=Field(default_factory=list)completed:bool=False
[docs]classCell(BaseModel):"""A cell on the Flow Free board. Attributes: position: Position of the cell. flow_id: ID of the flow occupying this cell, if any. is_endpoint: Whether this cell contains an endpoint. pipe_direction: Direction of the pipe in this cell, if any. """position:Positionflow_id:str|None=Noneis_endpoint:bool=Falsepipe_direction:PipeDirection|None=None
[docs]classFlowFreeState(SinglePlayerGameState):"""State for the Flow Free game. Attributes: rows: Number of rows in the grid. cols: Number of columns in the grid. grid: 2D grid of cells. flows: Dictionary of flows by ID. current_flow_id: ID of the currently selected flow. puzzle_id: Identifier for the current puzzle. hints_used: Number of hints used. """rows:int=Field(default=5,description="Number of rows in the grid")cols:int=Field(default=5,description="Number of columns in the grid")grid:list[list[Cell]]=Field(default_factory=list,description="2D grid of cells")flows:dict[str,Flow]=Field(default_factory=dict,description="Dictionary of flows by ID")current_flow_id:str|None=Field(default=None,description="ID of the currently selected flow")puzzle_id:str=Field(default_factory=lambda:str(uuid4()),description="Identifier for the current puzzle",)@computed_field@propertydefis_solved(self)->bool:"""Check if the puzzle is solved. The puzzle is solved when all flows are completed and all cells are filled. """# Check if all flows are completedifnotall(flow.completedforflowinself.flows.values()):returnFalse# Check if all cells are filledforrowinself.grid:forcellinrow:ifcell.flow_idisNone:returnFalsereturnTrue@computed_field@propertydefcompletion_percentage(self)->float:"""Calculate the percentage of flows completed."""ifnotself.flows:return0.0return(sum(1forflowinself.flows.values()ifflow.completed)/len(self.flows)*100.0)@computed_field@propertydeftotal_cells(self)->int:"""Calculate the total number of cells on the board."""returnself.rows*self.cols@computed_field@propertydeffilled_cells(self)->int:"""Calculate the number of filled cells on the board."""returnsum(1forrowinself.gridforcellinrowifcell.flow_idisnotNone)@computed_field@propertydefboard_fill_percentage(self)->float:"""Calculate the percentage of the board that is filled."""ifself.total_cells==0:return0.0returnself.filled_cells/self.total_cells*100.0
[docs]defget_cell(self,position:Position)->Cell|None:"""Get the cell at the specified position. Args: position: Position to get the cell for. Returns: The cell at the position, or None if out of bounds. """if0<=position.row<self.rowsand0<=position.col<self.cols:returnself.grid[position.row][position.col]returnNone
[docs]defis_cell_empty(self,position:Position)->bool:"""Check if a cell is empty. Args: position: Position to check. Returns: True if the cell is empty, False otherwise. """cell=self.get_cell(position)returncellisnotNoneandcell.flow_idisNone
[docs]defis_cell_endpoint(self,position:Position)->bool:"""Check if a cell contains an endpoint. Args: position: Position to check. Returns: True if the cell contains an endpoint, False otherwise. """cell=self.get_cell(position)returncellisnotNoneandcell.is_endpoint
[docs]defget_adjacent_positions(self,position:Position)->list[Position]:"""Get all valid adjacent positions. Args: position: Position to get adjacent positions for. Returns: List of adjacent positions. """adjacent=[]fordr,dcin[(0,1),(1,0),(0,-1),(-1,0)]:# right, down, left, upnew_row,new_col=position.row+dr,position.col+dcif0<=new_row<self.rowsand0<=new_col<self.cols:adjacent.append(Position(row=new_row,col=new_col))returnadjacent
[docs]defto_display_string(self)->str:"""Generate a string representation of the board for display. Returns: A formatted string representation of the board. """result=[]# Header rowheader=" "+" ".join(str(i)foriinrange(self.cols))result.append(header)# Separatorseparator=" +"+"-"*(2*self.cols-1)+"+"result.append(separator)# Board rowsforrinrange(self.rows):row_str=f"{r} |"forcinrange(self.cols):cell=self.grid[r][c]ifcell.is_endpoint:row_str+="O"elifcell.flow_idisnotNone:row_str+="#"else:row_str+=" "ifc<self.cols-1:row_str+=" "row_str+="|"result.append(row_str)# Bottom separatorresult.append(separator)# Flow informationresult.append("\nFlows:")for_flow_id,flowinself.flows.items():status="✓"ifflow.completedelse" "result.append(f" {status}{flow.color}: {flow.start.position} → {flow.end.position}")return"\n".join(result)