import pygame
import sys
import math
import random
import heapq

# Initialize Pygame
pygame.init()

# Screen settings
WIDTH, HEIGHT = 800, 600
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Enhanced Turn-Based AI Fighter with Pathfinding and Trajectory Lines")
CLOCK = pygame.time.Clock()
FPS = 60

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (220, 20, 60)
BLUE = (30, 144, 255)
GREEN = (0, 255, 0)
GREY = (200, 200, 200)
LIGHT_GREY = (230, 230, 230)
PURPLE = (128, 0, 128)
ORANGE = (255, 165, 0)
YELLOW = (255, 255, 0)

# Font
FONT = pygame.font.SysFont(None, 24)

# Game Settings
ATTACK_RANGE = 1  # In grid cells
ATTACK_DAMAGE = 20
MOVE_DISTANCE = 1  # Cells per move action
TURN_POINTS = 4
GRID_SIZE = 40  # Size of each grid cell in pixels
ROWS = HEIGHT // GRID_SIZE
COLS = WIDTH // GRID_SIZE

# Potion Settings
POTION_DAMAGE = 5
POTION_DURATION = 3  # Number of turns
POTION_COST = 2
POTION_RANGE = 1  # AoE radius in grid cells

# Floating Text Settings
FLOATING_TEXT_LIFETIME = 1000  # milliseconds
FLOATING_TEXT_SPEED = 0.5  # pixels per frame

# Trajectory Line Settings
TRAJECTORY_LIFETIME = 1000  # milliseconds
TRAJECTORY_COLOR = YELLOW
TRAJECTORY_WIDTH = 2

class FloatingText:
    def __init__(self, text, position, color=BLACK):
        self.text = FONT.render(text, True, color)
        self.position = list(position)
        self.start_time = pygame.time.get_ticks()
        self.alpha = 255
        self.color = color
        self.text.set_alpha(self.alpha)

    def update(self):
        # Move text upward
        self.position[1] -= FLOATING_TEXT_SPEED
        # Fade out
        elapsed = pygame.time.get_ticks() - self.start_time
        if elapsed < FLOATING_TEXT_LIFETIME:
            self.alpha = max(255 - (255 * elapsed) // FLOATING_TEXT_LIFETIME, 0)
            self.text.set_alpha(self.alpha)
            return True
        return False

    def draw(self, surface):
        surface.blit(self.text, self.position)

class TrajectoryLine:
    def __init__(self, start_pos, end_pos):
        self.start_pos = start_pos
        self.end_pos = end_pos
        self.start_time = pygame.time.get_ticks()

    def update(self):
        # Check if trajectory line should still be displayed
        elapsed = pygame.time.get_ticks() - self.start_time
        if elapsed < TRAJECTORY_LIFETIME:
            return True
        return False

    def draw(self, surface):
        pygame.draw.line(surface, TRAJECTORY_COLOR, self.start_pos, self.end_pos, TRAJECTORY_WIDTH)

class Obstacle:
    def __init__(self, grid_pos):
        self.grid_x, self.grid_y = grid_pos

    def draw(self, surface):
        rect = pygame.Rect(self.grid_x * GRID_SIZE, self.grid_y * GRID_SIZE, GRID_SIZE, GRID_SIZE)
        pygame.draw.rect(surface, GREY, rect)

class PotionArea:
    def __init__(self, center_pos, duration=POTION_DURATION, damage=POTION_DAMAGE):
        self.center_x, self.center_y = center_pos
        self.duration = duration
        self.damage = damage
        self.affected_cells = self.calculate_aoe()

    def calculate_aoe(self):
        cells = []
        for dx in range(-POTION_RANGE, POTION_RANGE + 1):
            for dy in range(-POTION_RANGE, POTION_RANGE + 1):
                x = self.center_x + dx
                y = self.center_y + dy
                if 0 <= x < COLS and 0 <= y < ROWS:
                    cells.append((x, y))
        return cells

    def apply_effect(self, fighters, floating_texts):
        for fighter in fighters:
            if (fighter.grid_x, fighter.grid_y) in self.affected_cells:
                fighter.health -= self.damage
                floating_texts.append(FloatingText(f"-{self.damage}", (fighter.pixel_x, fighter.pixel_y - fighter.radius - 20), PURPLE))
                print(f"{fighter.name} is poisoned for {self.damage} damage! Health: {fighter.health}")

    def draw(self, surface):
        for cell in self.affected_cells:
            rect = pygame.Rect(cell[0] * GRID_SIZE, cell[1] * GRID_SIZE, GRID_SIZE, GRID_SIZE)
            pygame.draw.rect(surface, ORANGE, rect, 3)  # Orange border to indicate potion area

class Fighter:
    def __init__(self, name, color, grid_pos):
        self.name = name
        self.color = color
        self.grid_x, self.grid_y = grid_pos
        self.health = 100
        self.radius = GRID_SIZE // 2 - 5
        self.pixel_x = self.grid_x * GRID_SIZE + GRID_SIZE // 2
        self.pixel_y = self.grid_y * GRID_SIZE + GRID_SIZE // 2
        self.potions_used = 0  # Number of potions used
        self.path = []  # Path for pathfinding

    def distance_to(self, other):
        return abs(self.grid_x - other.grid_x) + abs(self.grid_y - other.grid_y)

    def move_along_path(self, obstacles):
        if self.path:
            next_cell = self.path.pop(0)
            if next_cell not in obstacles:
                self.grid_x, self.grid_y = next_cell
                self.update_pixel_position()
            else:
                # Obstacle encountered, clear path
                self.path = []

    def attack(self, target, floating_texts):
        target.health -= ATTACK_DAMAGE
        floating_texts.append(FloatingText(f"-{ATTACK_DAMAGE}", (target.pixel_x, target.pixel_y - target.radius - 20), RED))
        print(f"{self.name} attacks {target.name} for {ATTACK_DAMAGE} damage! {target.name} Health: {target.health}")

    def use_potion(self, target_pos, potions, floating_texts, trajectories):
        potion = PotionArea(target_pos)
        potions.append(potion)
        self.potions_used += 1
        print(f"{self.name} uses a potion at {target_pos}!")
        # Add trajectory line
        start_pixel = (self.pixel_x, self.pixel_y)
        end_pixel = (target_pos[0] * GRID_SIZE + GRID_SIZE // 2, target_pos[1] * GRID_SIZE + GRID_SIZE // 2)
        trajectories.append(TrajectoryLine(start_pixel, end_pixel))

    def update_pixel_position(self):
        self.pixel_x = self.grid_x * GRID_SIZE + GRID_SIZE // 2
        self.pixel_y = self.grid_y * GRID_SIZE + GRID_SIZE // 2

    def draw(self, surface):
        pygame.draw.circle(surface, self.color, (self.pixel_x, self.pixel_y), self.radius)
        # Draw health bar
        health_bar_width = GRID_SIZE - 10
        health_bar_height = 5
        health_ratio = max(self.health / 100, 0)
        pygame.draw.rect(surface, RED, (self.pixel_x - health_bar_width//2, self.pixel_y - self.radius - 15, health_bar_width, health_bar_height))
        pygame.draw.rect(surface, GREEN, (self.pixel_x - health_bar_width//2, self.pixel_y - self.radius - 15, int(health_bar_width * health_ratio), health_bar_height))

class Game:
    def __init__(self):
        self.fighter1 = Fighter("Red Fighter", RED, (1, ROWS // 2))
        self.fighter2 = Fighter("Blue Fighter", BLUE, (COLS - 2, ROWS // 2))
        self.current_turn = self.fighter1
        self.other = self.fighter2
        self.turn_points = TURN_POINTS
        self.game_over = False
        self.winner = None
        self.action_queue = []
        self.action_delay = 500  # milliseconds
        self.last_action_time = pygame.time.get_ticks()
        self.actions_executed = 0
        self.floating_texts = []
        self.potions = []
        self.trajectories = []
        self.obstacles = self.generate_obstacles()

    def generate_obstacles(self):
        # Generate some random obstacles for demonstration
        obstacle_positions = set()
        # Create a vertical wall in the middle
        for y in range(ROWS // 2 - 2, ROWS // 2 + 3):
            obstacle_positions.add((COLS//2, y))
        # Add some random obstacles
        while len(obstacle_positions) < 15:
            x = random.randint(1, COLS - 2)
            y = random.randint(1, ROWS - 2)
            obstacle_positions.add((x, y))
        return obstacle_positions

    def switch_turn(self):
        self.current_turn, self.other = self.other, self.current_turn
        self.turn_points = TURN_POINTS
        self.action_queue = []
        self.actions_executed = 0

    def ai_decision(self):
        actions = []
        points = self.turn_points

        # Simple AI: Prioritize using potions, then attacking, then moving
        distance = self.current_turn.distance_to(self.other)

        # Decide whether to use a potion
        if self.current_turn.potions_used < 2 and points >= POTION_COST:
            # Decide to place a potion near the opponent
            target_x = self.other.grid_x
            target_y = self.other.grid_y
            # Choose a nearby cell that is not an obstacle
            possible_positions = [
                (target_x, target_y),
                (target_x + 1, target_y),
                (target_x -1, target_y),
                (target_x, target_y +1),
                (target_x, target_y -1)
            ]
            random.shuffle(possible_positions)
            for pos in possible_positions:
                if 0 <= pos[0] < COLS and 0 <= pos[1] < ROWS and pos not in self.obstacles:
                    actions.append(('use_potion', pos))
                    points -= POTION_COST
                    break

        # Attack if in range
        while points >= 2 and self.current_turn.distance_to(self.other) <= ATTACK_RANGE:
            actions.append(('attack', None))
            points -=2

        # Move towards if not in range
        if points >=1 and self.current_turn.distance_to(self.other) > ATTACK_RANGE:
            # Compute path using A*
            path = self.find_path((self.current_turn.grid_x, self.current_turn.grid_y),
                                  (self.other.grid_x, self.other.grid_y),
                                  self.obstacles)
            if path and len(path) >1:
                next_step = path[1]  # path[0] is current position
                actions.append(('move', next_step))
                points -=1

        return actions

    def enqueue_actions(self, actions):
        self.action_queue.extend(actions)

    def execute_next_action(self):
        if self.actions_executed >= len(self.action_queue):
            # No more actions to execute
            self.switch_turn()
            return

        action, param = self.action_queue[self.actions_executed]
        if action == 'attack':
            if self.current_turn.distance_to(self.other) <= ATTACK_RANGE:
                self.current_turn.attack(self.other, self.floating_texts)
                if self.other.health <=0:
                    self.game_over = True
                    self.winner = self.current_turn
        elif action == 'move':
            # Move to the specified cell
            if param and param not in self.obstacles:
                self.current_turn.grid_x, self.current_turn.grid_y = param
                self.current_turn.update_pixel_position()
        elif action == 'use_potion':
            self.current_turn.use_potion(param, self.potions, self.floating_texts, self.trajectories)
        self.actions_executed +=1

    def apply_potions(self):
        for potion in self.potions[:]:
            potion.apply_effect([self.fighter1, self.fighter2], self.floating_texts)
            potion.duration -=1
            if potion.duration <=0:
                self.potions.remove(potion)

    def draw_grid(self, surface):
        for x in range(0, WIDTH, GRID_SIZE):
            pygame.draw.line(surface, LIGHT_GREY, (x, 0), (x, HEIGHT))
        for y in range(0, HEIGHT, GRID_SIZE):
            pygame.draw.line(surface, LIGHT_GREY, (0, y), (WIDTH, y))
        # Draw obstacles
        for pos in self.obstacles:
            obstacle = Obstacle(pos)
            obstacle.draw(surface)

    def draw_turn_indicator(self, surface):
        text = FONT.render(f"Turn: {self.current_turn.name}", True, BLACK)
        surface.blit(text, (WIDTH//2 - text.get_width()//2, 10))

    def find_path(self, start, goal, obstacles):
        """
        Implements the A* pathfinding algorithm.
        Returns a list of tuples representing the path from start to goal.
        """
        def heuristic(a, b):
            return abs(a[0] - b[0]) + abs(a[1] - b[1])

        open_set = []
        heapq.heappush(open_set, (0, start))
        came_from = {}
        g_score = {start: 0}
        f_score = {start: heuristic(start, goal)}

        while open_set:
            current = heapq.heappop(open_set)[1]
            if current == goal:
                # Reconstruct path
                path = [current]
                while current in came_from:
                    current = came_from[current]
                    path.append(current)
                path.reverse()
                return path

            neighbors = self.get_neighbors(current, obstacles)
            for neighbor in neighbors:
                tentative_g_score = g_score[current] + 1  # All moves cost the same
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + heuristic(neighbor, goal)
                    if neighbor not in [i[1] for i in open_set]:
                        heapq.heappush(open_set, (f_score[neighbor], neighbor))

        return None  # No path found

    def get_neighbors(self, cell, obstacles):
        x, y = cell
        neighbors = []
        directions = [(-1,0), (1,0), (0,-1), (0,1)]
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            if 0 <= nx < COLS and 0 <= ny < ROWS and (nx, ny) not in obstacles:
                neighbors.append((nx, ny))
        return neighbors

    def run(self):
        # Initial AI decision
        if not self.game_over:
            actions = self.ai_decision()
            self.enqueue_actions(actions)

        while True:
            CLOCK.tick(FPS)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

            current_time = pygame.time.get_ticks()

            if not self.game_over and self.action_queue:
                if current_time - self.last_action_time >= self.action_delay:
                    self.execute_next_action()
                    self.last_action_time = current_time

            # Apply potion effects at the start of each turn
            if not self.game_over and self.actions_executed >= len(self.action_queue) and self.action_queue:
                self.apply_potions()

            # Drawing
            SCREEN.fill(WHITE)
            self.draw_grid(SCREEN)
            self.fighter1.draw(SCREEN)
            self.fighter2.draw(SCREEN)

            # Draw potions
            for potion in self.potions:
                potion.draw(SCREEN)

            # Draw trajectory lines
            for traj in self.trajectories[:]:
                if traj.update():
                    traj.draw(SCREEN)
                else:
                    self.trajectories.remove(traj)

            # Draw floating texts
            for text in self.floating_texts[:]:
                if text.update():
                    text.draw(SCREEN)
                else:
                    self.floating_texts.remove(text)

            if not self.game_over:
                self.draw_turn_indicator(SCREEN)
            else:
                winner_text = FONT.render(f"{self.winner.name} Wins!", True, BLACK)
                SCREEN.blit(winner_text, (WIDTH//2 - winner_text.get_width()//2, HEIGHT//2 - winner_text.get_height()//2))

            pygame.display.flip()

            if not self.game_over and not self.action_queue:
                # Prepare next turn
                self.apply_potions()
                if self.fighter1.health <=0:
                    self.game_over = True
                    self.winner = self.fighter2
                elif self.fighter2.health <=0:
                    self.game_over = True
                    self.winner = self.fighter1
                if not self.game_over:
                    actions = self.ai_decision()
                    self.enqueue_actions(actions)

if __name__ == "__main__":
    game = Game()
    game.run()

Add more fighters, each use different tactic, provide full updated code