# Property Tycoon Player.py
# Contains the classes for the players, such as the money, the properties, and the position.
import pygame
import math
import os
from src.Font_Manager import font_manager
WHITE = (255, 255, 255)
HUMAN_COLOR = (75, 139, 190)
AI_COLOR = (190, 75, 75)
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
[docs]
class Player:
def __init__(self, name, player_number=1, is_ai=False, ai_difficulty="easy"):
self.position = 1
self.money = 1500
self.properties = []
self.is_ai = is_ai
if is_ai and not name.startswith("ai-"):
self.name = f"ai-{name}"
else:
self.name = name
self.player_number = player_number
self.ai_difficulty = ai_difficulty
self.rect = pygame.Rect(0, 0, 40, 40)
self.color = AI_COLOR if is_ai else HUMAN_COLOR
self.in_jail = False
self.jail_turns = 0
self.jail_cards = 0
self.stay_in_jail = False
self.bankrupt = False
self.voluntary_exit = False
self.final_assets = 0
self.animation_offset = 0
self.animation_time = 0
self.highlight_intensity = 0
self.is_active = False
self.is_winner = False
self.bounce_offset = 0
self.bounce_speed = 0.15
self.glow_alpha = 0
self.target_animation_offset = 0
self.is_moving = False
self.move_start_position = 1
self.move_target_position = 1
self.move_progress = 0.0
self.move_speed = 0.1
self.move_path = []
self.current_path_index = 0
if self.is_ai:
from src.Ai_Player_Logic import EasyAIPlayer, HardAIPlayer
if ai_difficulty.lower() == "hard":
self.ai_controller = HardAIPlayer()
print(
f"Initialized Hard AI controller for {self.name} with emotion system"
)
else:
self.ai_controller = EasyAIPlayer()
print(f"Initialized Easy AI controller for {self.name}")
self.load_player_image()
print(f"Player {name} initial position: {self.position}")
# player image load
[docs]
def load_player_image(self):
try:
image_path = os.path.join(
base_path, "assets", "image", f"Playertoken ({self.player_number}).png"
)
print(f"Attempting to load player image from: {image_path}")
if os.path.exists(image_path):
self.player_image = pygame.image.load(image_path)
self.player_image = pygame.transform.scale(self.player_image, (40, 40))
print(f"Successfully loaded player {self.player_number} image")
else:
print(f"Image file not found at {image_path}, trying absolute path...")
current_dir = os.getcwd()
abs_path = os.path.join(
current_dir,
"assets",
"image",
f"Playertoken ({self.player_number}).png",
)
if os.path.exists(abs_path):
self.player_image = pygame.image.load(abs_path)
self.player_image = pygame.transform.scale(
self.player_image, (40, 40)
)
print(
f"Successfully loaded player {self.player_number} image from absolute path"
)
else:
print(f"Image file not found at {abs_path}")
self.create_fallback_token()
except pygame.error as e:
print(f"Could not load player image {self.player_number}: {e}")
self.create_fallback_token()
# fallback token gen
[docs]
def create_fallback_token(self):
self.player_image = pygame.Surface((40, 40), pygame.SRCALPHA)
token_color = (*self.color[:3], 230)
highlight_color = tuple(min(c + 40, 255) for c in self.color[:3])
pygame.draw.circle(self.player_image, token_color, (20, 20), 20)
for i in range(10):
alpha = int(100 * (1 - i / 10))
pygame.draw.circle(
self.player_image, (*highlight_color, alpha), (15, 15), 20 - i
)
number_font = font_manager.get_font(28)
number_text = number_font.render(str(self.player_number), True, WHITE)
number_rect = number_text.get_rect(center=(20, 20))
shadow_text = number_font.render(str(self.player_number), True, (0, 0, 0))
shadow_rect = number_rect.copy()
shadow_rect.x += 1
shadow_rect.y += 1
self.player_image.blit(shadow_text, shadow_rect)
self.player_image.blit(number_text, number_rect)
print(f"Created enhanced fallback token for player {self.player_number}")
# animation update logic
[docs]
def update_animation(self):
current_time = pygame.time.get_ticks()
if self.is_active:
self.target_animation_offset = 5
self.highlight_intensity = min(self.highlight_intensity + 0.1, 1.0)
else:
self.target_animation_offset = 0
self.highlight_intensity = max(self.highlight_intensity - 0.1, 0.0)
self.animation_offset += (
self.target_animation_offset - self.animation_offset
) * 0.2
if self.is_winner:
self.bounce_offset = math.sin(current_time * self.bounce_speed) * 8
self.glow_alpha = abs(math.sin(current_time * 0.003)) * 255
else:
self.bounce_offset = 0
self.glow_alpha = 0
if self.is_moving:
if self.current_path_index < len(self.move_path):
self.move_progress += self.move_speed
if self.move_progress > 1.0:
next_position = self.move_path[self.current_path_index]
if 1 <= next_position <= 40:
self.position = next_position
else:
self.position = 1
self.current_path_index += 1
if self.current_path_index >= len(self.move_path):
self.is_moving = False
if 1 <= self.move_target_position <= 40:
self.position = self.move_target_position
else:
self.position = 1
else:
self.is_moving = False
if 1 <= self.move_target_position <= 40:
self.position = self.move_target_position
else:
self.position = 1
[docs]
def get_total_offset(self):
return self.animation_offset + self.bounce_offset
[docs]
def set_active(self, active):
self.is_active = active
[docs]
def set_winner(self, winner):
self.is_winner = winner
# player draw logic
[docs]
def draw_player(self, screen, x, y):
if not (1 <= self.position <= 40):
print(
f"Warning: Invalid position {self.position} detected in draw_player for {self.name}, resetting to position 1"
)
self.position = 1
print(f"Drawing player {self.name} at screen coordinates: ({x}, {y})")
current_time = pygame.time.get_ticks()
self.animation_offset = abs(math.sin(current_time * 0.003)) * 5
self.rect.x = x
self.rect.y = y - self.animation_offset
if self.voluntary_exit:
exit_font = font_manager.get_font(12)
exit_text = exit_font.render("EXITED", True, (200, 0, 0))
exit_rect = exit_text.get_rect(center=(self.rect.centerx, self.rect.y - 15))
screen.blit(exit_text, exit_rect)
glow_surface = pygame.Surface(
(self.rect.width + 8, self.rect.height + 8), pygame.SRCALPHA
)
for i in range(4):
alpha = int(50 * (1 - i / 4))
pygame.draw.circle(
glow_surface,
(*self.color[:3], alpha),
(self.rect.width // 2 + 4, self.rect.height // 2 + 4),
self.rect.width // 2 - i,
)
screen.blit(glow_surface, (self.rect.x - 4, self.rect.y - 4))
if hasattr(self, "player_image") and self.player_image is not None:
exited_image = self.player_image.copy()
exited_image.set_alpha(128)
screen.blit(exited_image, self.rect)
else:
self.create_fallback_token()
exited_image = self.player_image.copy()
exited_image.set_alpha(128)
screen.blit(exited_image, self.rect)
elif self.bankrupt:
bankrupt_font = font_manager.get_font(12)
bankrupt_text = bankrupt_font.render("BANKRUPT", True, (200, 0, 0))
bankrupt_rect = bankrupt_text.get_rect(
center=(self.rect.centerx, self.rect.y - 15)
)
screen.blit(bankrupt_text, bankrupt_rect)
glow_surface = pygame.Surface(
(self.rect.width + 8, self.rect.height + 8), pygame.SRCALPHA
)
for i in range(4):
alpha = int(50 * (1 - i / 4))
pygame.draw.circle(
glow_surface,
(*self.color[:3], alpha),
(self.rect.width // 2 + 4, self.rect.height // 2 + 4),
self.rect.width // 2 - i,
)
screen.blit(glow_surface, (self.rect.x - 4, self.rect.y - 4))
if hasattr(self, "player_image") and self.player_image is not None:
bankrupt_image = self.player_image.copy()
bankrupt_image.set_alpha(128)
screen.blit(bankrupt_image, self.rect)
else:
self.create_fallback_token()
bankrupt_image = self.player_image.copy()
bankrupt_image.set_alpha(128)
screen.blit(bankrupt_image, self.rect)
else:
glow_surface = pygame.Surface(
(self.rect.width + 8, self.rect.height + 8), pygame.SRCALPHA
)
for i in range(4):
alpha = int(100 * (1 - i / 4))
pygame.draw.circle(
glow_surface,
(*self.color[:3], alpha),
(self.rect.width // 2 + 4, self.rect.height // 2 + 4),
self.rect.width // 2 - i,
)
screen.blit(glow_surface, (self.rect.x - 4, self.rect.y - 4))
if hasattr(self, "player_image") and self.player_image is not None:
print(
f"Drawing player {self.player_number} image at position: ({self.rect.x}, {self.rect.y})"
)
screen.blit(self.player_image, self.rect)
else:
print(f"No image for player {self.player_number}, creating fallback")
self.create_fallback_token()
screen.blit(self.player_image, self.rect)
# player move logic
[docs]
def move(self, steps):
if self.is_moving:
return
if not (1 <= self.position <= 40):
print(
f"Warning: Invalid position {self.position} detected for {self.name} before move, resetting to position 1"
)
self.position = 1
if not isinstance(steps, int) or steps < 0 or steps > 40:
print(
f"Warning: Invalid steps value {steps} for {self.name}, adjusting to valid range"
)
steps = max(0, min(steps, 40))
self.move_start_position = self.position
new_position = ((self.position - 1 + steps) % 40) + 1
print(
f"Player.move: {self.name} from {self.position} to {new_position} ({steps} steps)"
)
if 1 <= new_position <= 40:
self.move_target_position = new_position
self.generate_move_path(steps)
self.is_moving = True
self.move_progress = 0.0
self.current_path_index = 0
self.animation_time = pygame.time.get_ticks()
else:
print(
f"Error: Invalid move resulting in position {new_position} for player {self.name}, resetting to position 1"
)
self.position = 1
self.move_target_position = 1
[docs]
def start_move(self, path):
if self.is_moving:
return
if not path:
print(
f"Warning: Empty path provided for {self.name}, not starting movement"
)
return
self.move_start_position = self.position
self.move_target_position = path[-1] if path else self.position
print(
f"Player.start_move: {self.name} from {self.position} to {self.move_target_position} via path {path}"
)
if path and all(1 <= pos <= 40 for pos in path):
self.move_path = path
self.is_moving = True
self.move_progress = 0.0
self.current_path_index = 0
self.animation_time = pygame.time.get_ticks()
else:
print(f"Error: Invalid path for player {self.name}: {path}")
self.is_moving = False
[docs]
def generate_move_path(self, steps):
self.move_path = []
if not isinstance(steps, int) or steps < 0 or steps > 40:
print(
f"Warning: Invalid steps value {steps} for {self.name} in generate_move_path, adjusting to valid range"
)
steps = max(0, min(steps, 40))
if not (1 <= self.position <= 40):
print(
f"Warning: Invalid position {self.position} detected in generate_move_path for {self.name}, resetting to position 1"
)
self.position = 1
current_pos = self.position
for i in range(steps):
next_pos = ((current_pos + 1 - 1) % 40) + 1
if not (1 <= next_pos <= 40):
print(
f"Warning: Invalid position {next_pos} calculated in path generation for {self.name}, resetting to position 1"
)
next_pos = 1
self.move_path.append(next_pos)
current_pos = next_pos
print(f"Generated move path for {self.name}: {self.move_path}")
[docs]
def is_animation_complete(self):
return not self.is_moving
# jail handling logic
[docs]
def pay(self, amount):
if self.money >= amount:
self.money -= amount
return True
return False
[docs]
def receive(self, amount):
self.money += amount
[docs]
def buy_property(self, property):
if self.money >= property.price:
self.money -= property.price
self.properties.append(property)
property.owner = self
print(f"{self.name} bought {property.name}!")
return True
else:
print(f"{self.name} doesn't have enough money to buy {property.name}!")
return False
[docs]
def add_jail_card(self, card_type):
self.jail_cards.append(card_type)
[docs]
def use_jail_card(self):
if self.jail_cards:
card_type = self.jail_cards.pop()
self.in_jail = False
self.jail_turns = 0
return card_type
return None
[docs]
def handle_jail_turn(self):
if not self.in_jail:
return False
if self.is_ai:
import random
if self.jail_cards and random.random() < 0.7:
return self.use_jail_card()
elif self.money >= 50 and random.random() < 0.5:
self.pay(50)
self.in_jail = False
self.jail_turns = 0
return None
self.jail_turns += 1
if self.jail_turns >= 3:
if self.money >= 50:
self.pay(50)
self.in_jail = False
self.jail_turns = 0
return None
# property management
[docs]
def can_afford(self, amount):
return self.money >= amount
[docs]
def add_property(self, property):
if property not in self.properties:
self.properties.append(property)
property.owner = self
[docs]
def remove_property(self, property):
if property in self.properties:
self.properties.remove(property)
property.owner = None
[docs]
def get_total_assets(self):
total = self.money
for prop in self.properties:
if not prop.mortgaged:
total += prop.price
else:
total += prop.price // 2
if hasattr(prop, "houses") and prop.houses > 0:
house_value = prop.get_house_sale_value()
total += house_value * prop.houses
if hasattr(prop, "has_hotel") and prop.has_hotel:
hotel_value = prop.get_hotel_sale_value()
total += hotel_value
return total
[docs]
def get_mortgageable_properties(self):
return [
p
for p in self.properties
if not p.mortgaged and p.houses == 0 and not p.has_hotel
]
[docs]
def get_unmortgageable_properties(self):
return [p for p in self.properties if p.mortgaged]
[docs]
def get_properties_with_houses(self):
return [p for p in self.properties if p.houses > 0]
[docs]
def get_properties_with_hotels(self):
return [p for p in self.properties if p.has_hotel]
[docs]
def can_build_houses(self):
return any(p.can_build_house(self.properties) for p in self.properties)
[docs]
def can_build_hotels(self):
return any(p.can_build_hotel(self.properties) for p in self.properties)
# bankruptcy handling
[docs]
def handle_bankruptcy(self, creditor=None):
self.bankrupt = True
for prop in self.properties[:]:
if creditor:
self.remove_property(prop)
creditor.add_property(prop)
if prop.mortgaged:
unmortgage_cost = (
prop.get_unmortgage_cost() - prop.get_mortgage_value()
)
if creditor.can_afford(unmortgage_cost):
creditor.pay(unmortgage_cost)
prop.unmortgage()
else:
self.remove_property(prop)
prop.mortgaged = False
prop.houses = 0
prop.has_hotel = False
if creditor and self.money > 0:
creditor.receive(self.money)
self.money = 0
# voluntary exit logic
[docs]
def handle_voluntary_exit(self):
self.voluntary_exit = True
self.final_assets = self.get_total_assets()
for prop in list(self.properties):
self.remove_property(prop)
prop.mortgaged = False
prop.houses = 0
prop.has_hotel = False
self.money = 0