Source code for src.Board

# Base on PropertyTycoonCardData.xlsx from canvas
# script based on Eric's provided flowchart photo (flowchart.drawio.png)
# This file contains the Board class, which is used to manage the board in the Property Tycoon game.

import pygame
import math
import os
from src.Property import Property
from typing import Optional, List
from src.Loadexcel import load_property_data
from src.Font_Manager import font_manager

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)
UI_BG = (18, 18, 18)
ACCENT_COLOR = (75, 139, 190)
SUCCESS_COLOR = (40, 167, 69)
ERROR_COLOR = (220, 53, 69)

GROUP_COLORS = {
    "Brown": (102, 51, 0),
    "Blue": (0, 200, 255),
    "Purple": (128, 0, 128),
    "Orange": (255, 128, 0),
    "Red": (255, 0, 0),
    "Yellow": (255, 236, 93),
    "Green": (0, 153, 0),
    "Deep blue": (0, 0, 153),
    "Stations": (128, 128, 128),
    "Utilities": (192, 192, 192),
}


base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


[docs] class CameraControls: def __init__(self): self.zoom_level = 1.0 self.offset_x = 0 self.offset_y = 0 self.move_speed = 5 self.zoom_speed = 0.05 self.min_zoom = 0.5 self.max_zoom = 2.0
[docs] def handle_camera_controls(self, keys): if keys[pygame.K_PLUS] or keys[pygame.K_EQUALS]: self.zoom_level = min(self.max_zoom, self.zoom_level + self.zoom_speed) if keys[pygame.K_MINUS]: self.zoom_level = max(self.min_zoom, self.zoom_level - self.zoom_speed) adjusted_speed = self.move_speed * (1 / self.zoom_level) if keys[pygame.K_w] or keys[pygame.K_UP]: self.offset_y -= adjusted_speed if keys[pygame.K_s] or keys[pygame.K_DOWN]: self.offset_y += adjusted_speed if keys[pygame.K_a] or keys[pygame.K_LEFT]: self.offset_x -= adjusted_speed if keys[pygame.K_d] or keys[pygame.K_RIGHT]: self.offset_x += adjusted_speed window_size = pygame.display.get_surface().get_size() max_offset = window_size[0] // 4 self.offset_x = max(min(self.offset_x, max_offset), -max_offset) self.offset_y = max(min(self.offset_y, max_offset), -max_offset) return self.zoom_level, self.offset_x, self.offset_y
[docs] class Board: def __init__(self, players): self.players = players self.spaces = [None] * 40 self.properties_data = load_property_data() self.camera = CameraControls() self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) try: self.board_image = pygame.image.load( os.path.join(self.project_root, "assets/image/board.png") ) except (pygame.error, FileNotFoundError) as e: print(f"Could not load board image: {e}") self.board_image = None try: self.original_background = pygame.image.load( os.path.join(self.project_root, "assets/image/background.jpg") ) self.original_background = self.original_background.convert() self.background_image = self.original_background.copy() except (pygame.error, FileNotFoundError) as e: print(f"Could not load background image: {e}") self.original_background = None self.background_image = None self.board_rects = self._create_board_rects() self.messages = [] self.message_times = [] self.message_font = font_manager.get_font(15) self.price_font = font_manager.get_font(20) self.small_font = font_manager.get_font(14)
[docs] def add_message(self, text): if text is None: return max_chars = 35 if len(text) > max_chars: words = text.split() lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) if len(test_line) <= max_chars: current_line.append(word) else: lines.append(" ".join(current_line)) current_line = [word] if current_line: lines.append(" ".join(current_line)) for line in lines: self.messages.append(line) self.message_times.append(pygame.time.get_ticks()) else: self.messages.append(text) self.message_times.append(pygame.time.get_ticks()) while len(self.messages) > 9: self.messages.pop(0) self.message_times.pop(0)
def _create_board_rects(self): screen_info = pygame.display.Info() window_width = screen_info.current_w window_height = screen_info.current_h base_board_size = min(window_width, window_height) * 0.8 board_size = base_board_size * self.camera.zoom_level corner_size = board_size // 11 normal_width = corner_size normal_height = int(normal_width * (5 / 8)) start_x = ((window_width - board_size) // 2) + self.camera.offset_x start_y = ((window_height - board_size) // 2) + self.camera.offset_y rects = [] for i in range(11): width = corner_size if (i == 0 or i == 10) else normal_width height = corner_size if (i == 0 or i == 10) else normal_height x = start_x y = start_y + board_size - height - (i * normal_width) rects.append(pygame.Rect(x, y, width, height)) for i in range(11, 21): width = corner_size if i == 20 else normal_width height = corner_size if i == 20 else normal_height x = start_x + ((i - 10) * normal_width) y = start_y rects.append(pygame.Rect(x, y, width, height)) for i in range(21, 31): width = corner_size if i == 30 else normal_height height = corner_size if i == 30 else normal_width x = start_x + board_size - width y = start_y + ((i - 20) * normal_width) rects.append(pygame.Rect(x, y, width, height)) for i in range(31, 40): width = corner_size if i == 39 else normal_width height = corner_size if i == 39 else normal_height x = start_x + board_size - ((i - 29) * normal_width) y = start_y + board_size - height rects.append(pygame.Rect(x, y, width, height)) return rects
[docs] def update_board_positions(self): self.board_rects = self._create_board_rects() for player in self.players: if not isinstance(player.position, int) or not (1 <= player.position <= 40): print( f"Warning: Invalid position {player.position} detected for {player.name} in update_board_positions, resetting to position 1" ) player.position = 1 player_rect = self.board_rects[player.position - 1] player.rect = player_rect
[docs] def draw_player(self, screen, player, rect, index): is_corner = rect.width > rect.height + 10 or rect.height > rect.width + 10 row = (player.player_number - 1) // 2 col = (player.player_number - 1) % 2 base_padding = 12 if is_corner else 8 spacing = 35 if is_corner else 30 if player.is_moving and player.current_path_index < len(player.move_path): if player.current_path_index == 0: start_rect = self.board_rects[player.move_start_position - 1] next_rect = self.board_rects[player.move_path[0] - 1] else: start_rect = self.board_rects[ player.move_path[player.current_path_index - 1] - 1 ] next_rect = self.board_rects[ player.move_path[player.current_path_index] - 1 ] progress = player.move_progress start_x, start_y = self._get_player_position_on_rect( start_rect, player.player_number, is_corner, False ) target_x, target_y = self._get_player_position_on_rect( next_rect, player.player_number, is_corner, False ) pos_x = int(start_x + (target_x - start_x) * progress) pos_y = int(start_y + (target_y - start_y) * progress) board_margin = 10 pos_x = max( min(start_rect.x, next_rect.x) - board_margin, min( pos_x, max(start_rect.x + start_rect.width, next_rect.x + next_rect.width) + board_margin, ), ) pos_y = max( min(start_rect.y, next_rect.y) - board_margin, min( pos_y, max( start_rect.y + start_rect.height, next_rect.y + next_rect.height ) + board_margin, ), ) else: if rect.width > rect.height + 10: pos_x = rect.x + base_padding + (col * spacing) pos_y = rect.y + (rect.height - 40) // 2 + (row * spacing // 2) elif rect.height > rect.width + 10: pos_x = rect.x + (rect.width - 40) // 2 + (col * spacing // 2) pos_y = rect.y + base_padding + (row * spacing) else: pos_x = rect.x + base_padding + (col * spacing) pos_y = rect.y + base_padding + (row * spacing) pos_x = max(rect.x + 2, min(pos_x, rect.x + rect.width - 42)) pos_y = max(rect.y + 2, min(pos_y, rect.y + rect.height - 42)) glow_surface = pygame.Surface((44, 44), pygame.SRCALPHA) for i in range(4): alpha = int(100 * (1 - i / 4)) pygame.draw.rect( glow_surface, (*player.color[:3], alpha), pygame.Rect(i, i, 44 - i * 2, 44 - i * 2), border_radius=5, ) screen.blit(glow_surface, (pos_x - 2, pos_y - 2 - player.animation_offset)) if player.player_image: token_rect = pygame.Rect(pos_x, pos_y - player.animation_offset, 40, 40) screen.blit(player.player_image, token_rect) else: pygame.draw.circle( screen, player.color, (pos_x + 20, pos_y + 20 - player.animation_offset), 20, )
def _get_player_position_on_rect( self, rect, player_number, is_corner, apply_bounds=True ): row = (player_number - 1) // 2 col = (player_number - 1) % 2 base_padding = 12 if is_corner else 8 spacing = 35 if is_corner else 30 if rect.width > rect.height + 10: pos_x = rect.x + base_padding + (col * spacing) pos_y = rect.y + (rect.height - 40) // 2 + (row * spacing // 2) elif rect.height > rect.width + 10: pos_x = rect.x + (rect.width - 40) // 2 + (col * spacing // 2) pos_y = rect.y + base_padding + (row * spacing) else: pos_x = rect.x + base_padding + (col * spacing) pos_y = rect.y + base_padding + (row * spacing) if apply_bounds: pos_x = max(rect.x + 2, min(pos_x, rect.x + rect.width - 42)) pos_y = max(rect.y + 2, min(pos_y, rect.y + rect.height - 42)) return pos_x, pos_y
[docs] def draw(self, screen): window_width = screen.get_width() window_height = screen.get_height() base_board_size = int(window_height * 0.9) board_size = int(base_board_size * self.camera.zoom_level) board_size = max(1, board_size) game_surface = pygame.Surface((window_width, window_height)) game_surface.fill(WHITE) if self.original_background: bg_width, bg_height = self.original_background.get_size() bg_aspect = bg_width / bg_height window_aspect = window_width / window_height if window_aspect > bg_aspect: scaled_width = window_width scaled_height = int(scaled_width / bg_aspect) else: scaled_height = window_height scaled_width = int(scaled_height * bg_aspect) pos_x = (window_width - scaled_width) // 2 pos_y = (window_height - scaled_height) // 2 scaled_bg = pygame.transform.scale( self.original_background, (scaled_width, scaled_height) ) game_surface.blit(scaled_bg, (pos_x, pos_y)) else: game_surface.fill(UI_BG) keys = pygame.key.get_pressed() zoom, offset_x, offset_y = self.camera.handle_camera_controls(keys) self.camera.zoom_level = zoom self.camera.offset_x = offset_x self.camera.offset_y = offset_y self.update_board_positions() board_x = ((window_width - board_size) // 2) + self.camera.offset_x board_y = ((window_height - board_size) // 2) + self.camera.offset_y board_surface = pygame.Surface((board_size, board_size)) board_surface.fill(WHITE) if self.board_image: scaled_board = pygame.transform.smoothscale( self.board_image, (board_size, board_size) ) board_surface.blit(scaled_board, (0, 0)) game_surface.blit(board_surface, (board_x, board_y)) transparent_surface = pygame.Surface( (window_width, window_height), pygame.SRCALPHA ) for player in self.players: if not isinstance(player.position, int) or not (1 <= player.position <= 40): print( f"Warning: Invalid position {player.position} detected for {player.name} in draw, resetting to position 1" ) player.position = 1 if player.is_moving and player.current_path_index < len(player.move_path): rect_for_animation = None if player.current_path_index == 0: start_pos = player.move_start_position - 1 end_pos = player.move_path[0] - 1 else: start_pos = player.move_path[player.current_path_index - 1] - 1 end_pos = player.move_path[player.current_path_index] - 1 start_pos = max(0, min(start_pos, len(self.board_rects) - 1)) end_pos = max(0, min(end_pos, len(self.board_rects) - 1)) if 0 <= start_pos < len(self.board_rects) and 0 <= end_pos < len( self.board_rects ): rect_for_animation = self.board_rects[start_pos] is_corner = ( rect_for_animation.width > rect_for_animation.height + 10 or rect_for_animation.height > rect_for_animation.width + 10 ) self.draw_player( transparent_surface, player, rect_for_animation, player.player_number, ) else: pos_index = max(0, min(player.position - 1, len(self.board_rects) - 1)) player_rect = self.board_rects[pos_index] self.draw_player( transparent_surface, player, player_rect, player.player_number ) info_panel_width = 290 info_panel_height = 230 info_panel_x = 20 info_panel_y = window_height - info_panel_height - 20 shadow_depth = 6 panel_shadow = pygame.Surface( (info_panel_width + shadow_depth * 2, info_panel_height + shadow_depth * 2), pygame.SRCALPHA, ) for i in range(shadow_depth): alpha = int(120 * (1 - i / shadow_depth)) pygame.draw.rect( panel_shadow, (*BLACK, alpha), pygame.Rect( i, i, info_panel_width + shadow_depth * 2 - i * 2, info_panel_height + shadow_depth * 2 - i * 2, ), border_radius=12, ) transparent_surface.blit( panel_shadow, (info_panel_x - shadow_depth, info_panel_y - shadow_depth) ) info_panel = pygame.Surface( (info_panel_width, info_panel_height), pygame.SRCALPHA ) pygame.draw.rect( info_panel, (*UI_BG, 230), info_panel.get_rect(), border_radius=10 ) header_height = 30 header_rect = pygame.Rect(0, 0, info_panel_width, header_height) pygame.draw.rect(info_panel, ACCENT_COLOR, header_rect, border_radius=10) pygame.draw.rect( info_panel, ACCENT_COLOR, pygame.Rect(0, header_height - 10, info_panel_width, 10), ) title_font = font_manager.get_font(20) title_text = title_font.render("MESSAGE LOG", True, WHITE) title_rect = title_text.get_rect( center=(info_panel_width // 2, header_height // 2) ) info_panel.blit(title_text, title_rect) border_width = 2 pygame.draw.rect( info_panel, WHITE, info_panel.get_rect(), border_width, border_radius=10 ) transparent_surface.blit(info_panel, (info_panel_x, info_panel_y)) line_height = self.message_font.get_height() + 5 max_messages = (info_panel_height - header_height - 20) // line_height visible_messages = self.messages[-max_messages:] text_y = info_panel_y + header_height + 10 for message in visible_messages: text = self.message_font.render(message, True, WHITE) transparent_surface.blit(text, (info_panel_x + 15, text_y)) text_y += line_height game_surface.blit(transparent_surface, (0, 0)) screen.blit(game_surface, (0, 0))
[docs] def get_space(self, position): array_pos = (position - 1) % 40 if array_pos <= 9: mapped_pos = array_pos elif array_pos <= 19: mapped_pos = array_pos elif array_pos <= 29: mapped_pos = array_pos else: mapped_pos = array_pos if 0 <= mapped_pos < len(self.spaces): return self.spaces[mapped_pos] return None
[docs] def update_ownership(self, properties_data): for position_str, prop_data in properties_data.items(): try: position = int(position_str) array_pos = position - 1 if 0 <= array_pos < 40 and self.spaces[array_pos]: self.spaces[array_pos].owner = prop_data.get("owner") except (ValueError, TypeError) as e: print(f"Error updating ownership for position {position_str}: {e}") self.properties_data.update(properties_data)
[docs] def get_property_group(self, position): if str(position) in self.properties_data: return self.properties_data[str(position)].get("group") return None
[docs] def get_property_position(self, position): if not 1 <= position <= 40: return None pos_index = position - 1 if pos_index < len(self.board_rects): rect = self.board_rects[pos_index] return (rect.x + rect.width // 2, rect.y + rect.height // 2) return None
[docs] def board_to_screen(self, x, y): return (x, y)
[docs] def update_offset(self, dx, dy): self.camera.offset_x += dx self.camera.offset_y += dy window_size = pygame.display.get_surface().get_size() max_offset = window_size[0] // 4 self.camera.offset_x = max(min(self.camera.offset_x, max_offset), -max_offset) self.camera.offset_y = max(min(self.camera.offset_y, max_offset), -max_offset) self.update_board_positions()
[docs] def property_clicked(self, pos): for i, rect in enumerate(self.board_rects): expanded_rect = rect.inflate(10, 10) if expanded_rect.collidepoint(pos): return i + 1 return None