Below is the complete code combining the Flappy Bird game and the DQN agent. It isn't working, there is no proress after 1000 episodes which are done in like 20seconds. Make it work well, maybe add more agents into training for example 100?
import pygame
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import matplotlib.pyplot as plt

# Initialize Pygame
pygame.init()

# Screen dimensions
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 600
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Flappy Bird with DQN")

# Game parameters
PIPE_WIDTH = 80
PIPE_HEIGHT = 500
PIPE_GAP = 150
BIRD_SIZE = 30
GRAVITY = 0.5
FLAP_STRENGTH = -10
FPS = 60

# Colors
WHITE = (255, 255, 255)
GREEN = (0, 200, 0)
BLUE = (0, 0, 255)
RED = (200, 0, 0)

# Fonts
FONT = pygame.font.SysFont("Arial", 24)

# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Define the Bird class
class Bird:
    def __init__(self):
        self.x = 50
        self.y = SCREEN_HEIGHT // 2
        self.velocity = 0
        self.size = BIRD_SIZE

    def flap(self):
        self.velocity = FLAP_STRENGTH

    def update(self):
        self.velocity += GRAVITY
        self.y += self.velocity

    def get_rect(self):
        return pygame.Rect(self.x, self.y, self.size, self.size)

    def draw(self, screen):
        pygame.draw.rect(screen, BLUE, self.get_rect())

# Define the Pipe class
class Pipe:
    def __init__(self, x):
        self.x = x
        self.width = PIPE_WIDTH
        self.gap = PIPE_GAP
        self.speed = 3
        self.top = random.randint(50, SCREEN_HEIGHT - self.gap - 50)
        self.passed = False

    def update(self):
        self.x -= self.speed

    def get_upper_rect(self):
        return pygame.Rect(self.x, 0, self.width, self.top)

    def get_lower_rect(self):
        return pygame.Rect(self.x, self.top + self.gap, self.width, SCREEN_HEIGHT - (self.top + self.gap))

    def draw(self, screen):
        pygame.draw.rect(screen, GREEN, self.get_upper_rect())
        pygame.draw.rect(screen, GREEN, self.get_lower_rect())

# Define the Game Environment
class FlappyBirdEnv:
    def __init__(self, render=True):
        self.bird = Bird()
        self.pipes = []
        self.spawn_pipe()
        self.score = 0
        self.done = False
        self.render = render
        self.clock = pygame.time.Clock()

    def reset(self):
        self.bird = Bird()
        self.pipes = []
        self.spawn_pipe()
        self.score = 0
        self.done = False
        state = self.get_state()
        return state

    def spawn_pipe(self):
        pipe = Pipe(SCREEN_WIDTH + 100)
        self.pipes.append(pipe)

    def step(self, action):
        reward = 0.1  # Small reward for staying alive

        # Apply action
        if action == 1:
            self.bird.flap()

        # Update bird
        self.bird.update()

        # Update pipes
        remove_pipes = []
        for pipe in self.pipes:
            pipe.update()
            if pipe.x + pipe.width < 0:
                remove_pipes.append(pipe)
            # Check for score
            if not pipe.passed and pipe.x + pipe.width < self.bird.x:
                pipe.passed = True
                self.score += 1
                reward = 1  # Reward for passing a pipe
        for pipe in remove_pipes:
            self.pipes.remove(pipe)

        # Add new pipes
        if len(self.pipes) == 0 or self.pipes[-1].x < SCREEN_WIDTH - 200:
            self.spawn_pipe()

        # Check for collisions
        for pipe in self.pipes:
            if self.bird.get_rect().colliderect(pipe.get_upper_rect()) or self.bird.get_rect().colliderect(pipe.get_lower_rect()):
                self.done = True
                reward = -1
        if self.bird.y <= 0 or self.bird.y + self.bird.size >= SCREEN_HEIGHT:
            self.done = True
            reward = -1

        # Get next state
        next_state = self.get_state()

        # Render
        if self.render:
            self.render_game()

        return next_state, reward, self.done

    def get_state(self):
        # State can include bird y position, velocity, distance to next pipe, top and bottom of next pipe
        # Find the next pipe
        next_pipe = None
        for pipe in self.pipes:
            if pipe.x + pipe.width > self.bird.x:
                next_pipe = pipe
                break
        if next_pipe is None:
            next_pipe = self.pipes[0]

        distance = (next_pipe.x + next_pipe.width) - self.bird.x
        top = next_pipe.top
        bottom = next_pipe.top + next_pipe.gap
        velocity = self.bird.velocity

        state = np.array([self.bird.y, velocity, distance, top, bottom], dtype=np.float32)
        return state

    def render_game(self):
        SCREEN.fill(WHITE)
        self.bird.draw(SCREEN)
        for pipe in self.pipes:
            pipe.draw(SCREEN)
        score_text = FONT.render(f"Score: {self.score}", True, RED)
        SCREEN.blit(score_text, (10, 10))
        pygame.display.flip()
        self.clock.tick(FPS)

# Define the Replay Memory
class ReplayMemory:
    def __init__(self, capacity):
        self.memory = deque(maxlen=capacity)

    def push(self, transition):
        self.memory.append(transition)

    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)

# Define the DQN Network
class DQN(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(input_dim, 128)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, output_dim)

    def forward(self, x):
        out = self.relu(self.fc1(x))
        out = self.relu(self.fc2(out))
        out = self.fc3(out)
        return out

# Define the Agent
class DQNAgent:
    def __init__(self, state_dim, action_dim):
        self.action_dim = action_dim

        self.policy_net = DQN(state_dim, action_dim).to(device)
        self.target_net = DQN(state_dim, action_dim).to(device)
        self.target_net.load_state_dict(self.policy_net.state_dict())
        self.target_net.eval()

        self.memory = ReplayMemory(10000)
        self.optimizer = optim.Adam(self.policy_net.parameters(), lr=1e-3)
        self.criterion = nn.MSELoss()

        self.gamma = 0.99
        self.epsilon = 1.0
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995
        self.batch_size = 64
        self.target_update = 10
        self.steps_done = 0

    def select_action(self, state):
        self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
        if random.random() < self.epsilon:
            return random.randrange(self.action_dim)
        else:
            with torch.no_grad():
                state = torch.FloatTensor(state).unsqueeze(0).to(device)
                q_values = self.policy_net(state)
                return q_values.max(1)[1].item()

    def push_transition(self, state, action, reward, next_state, done):
        self.memory.push((state, action, reward, next_state, done))

    def update(self):
        if len(self.memory) < self.batch_size:
            return

        transitions = self.memory.sample(self.batch_size)
        batch_state, batch_action, batch_reward, batch_next_state, batch_done = zip(*transitions)

        batch_state = torch.FloatTensor(batch_state).to(device)
        batch_action = torch.LongTensor(batch_action).unsqueeze(1).to(device)
        batch_reward = torch.FloatTensor(batch_reward).unsqueeze(1).to(device)
        batch_next_state = torch.FloatTensor(batch_next_state).to(device)
        batch_done = torch.FloatTensor(batch_done).unsqueeze(1).to(device)

        current_q = self.policy_net(batch_state).gather(1, batch_action)
        next_q = self.target_net(batch_next_state).max(1)[0].unsqueeze(1)
        target_q = batch_reward + (1 - batch_done) * self.gamma * next_q

        loss = self.criterion(current_q, target_q)

        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

    def update_target_network(self):
        self.target_net.load_state_dict(self.policy_net.state_dict())

# Training parameters
NUM_EPISODES = 1000
MAX_STEPS = 1000

def train():
    env = FlappyBirdEnv(render=False)
    state_dim = 5
    action_dim = 2  # 0: do nothing, 1: flap
    agent = DQNAgent(state_dim, action_dim)

    scores = []
    moving_avg = []

    for episode in range(1, NUM_EPISODES + 1):
        state = env.reset()
        total_reward = 0
        for step in range(MAX_STEPS):
            action = agent.select_action(state)
            next_state, reward, done = env.step(action)
            agent.push_transition(state, action, reward, next_state, done)
            agent.update()
            state = next_state
            total_reward += reward
            if done:
                break

        scores.append(env.score)
        moving_avg.append(np.mean(scores[-100:]))

        # Update target network
        if episode % agent.target_update == 0:
            agent.update_target_network()

        print(f"Episode {episode} | Score: {env.score} | Epsilon: {agent.epsilon:.3f} | Avg Score: {moving_avg[-1]:.2f}")

        # Early stopping if moving average is good enough
        if moving_avg[-1] >= 100:
            print("Environment solved!")
            break

    # Plot the scores
    plt.figure(figsize=(12,5))
    plt.title("Training Progress")
    plt.plot(scores, label='Score per Episode')
    plt.plot(moving_avg, label='100-Episode Moving Average')
    plt.xlabel('Episode')
    plt.ylabel('Score')
    plt.legend()
    plt.show()

    # Save the trained model
    torch.save(agent.policy_net.state_dict(), "flappy_bird_dqn.pth")

    # After training, you can run the game with render=True
    run_game(agent)

def run_game(agent):
    env = FlappyBirdEnv(render=True)
    state = env.reset()
    done = False
    while not done:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return

        action = agent.select_action(state)
        state, reward, done = env.step(action)

    print(f"Final Score: {env.score}")
    pygame.quit()

if __name__ == "__main__":
    train()