diff --git a/README.md b/README.md
index c5bdb55..d409c58 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,129 @@
-# POOL GAME
-The Pool Game project is a Python-based endeavor utilizing libraries such as pygame and pymunk. This project aims to create a virtual representation of the classic game of pool, implementing realistic physics and interactive gameplay features for an engaging user experience.
+# 🎱 Pool Game
-## IMAGE DEMO
-
-
+[](https://www.python.org/)
+[](https://www.pygame.org/)
+[](http://www.pymunk.org/)
+[](LICENSE)
+
+Trò chơi **Bi-a 2D** mô phỏng vật lý thực tế, xây dựng bằng **Python** với **Pygame** (render đồ họa) và **Pymunk** (engine vật lý rigid-body). Người chơi sử dụng chuột để điều khiển cơ bi-a, tích lực bằng cách giữ chuột, và đánh tất cả 15 bi màu vào lỗ trước khi hết mạng.
+
+## 📸 Demo
+
+
+
-## CODE DEMO
-```python
-# create pool cue
-class Cue:
- def __init__(self, pos):
- self.original_image = cue_image
- self.angle = 0
- self.image = pygame.transform.rotate(self.original_image, self.angle)
- self.rect = self.image.get_rect()
- self.rect.center = pos
-
- def update(self, angle):
- self.angle = angle
-
- def draw(self, surface):
- self.image = pygame.transform.rotate(self.original_image, self.angle)
- surface.blit(
- self.image,
- (
- self.rect.centerx - self.image.get_width() / 2,
- self.rect.centery - self.image.get_height() / 2,
- ),
- )
+## 🏗️ Kiến trúc kỹ thuật
+
+### Hằng số cấu hình
+
+| Nhóm | Hằng số | Giá trị | Mô tả |
+|------|---------|---------|-------|
+| Cửa sổ | `SCREEN_WIDTH × SCREEN_HEIGHT` | 1200 × 678 | Kích thước bàn bi-a (pixel) |
+| Bi | `BALL_COUNT` | 16 | 15 bi màu + 1 bi trắng |
+| Bi | `BALL_MASS` / `BALL_ELASTICITY` | 5 / 0.8 | Khối lượng & đàn hồi |
+| Bàn | `FRICTION` | 1000 | Ma sát mô phỏng qua PivotJoint |
+| Bàn | `CUSHION_ELASTICITY` | 0.6 | Đàn hồi thành bàn |
+| Bắn | `MAX_FORCE` / `FORCE_STEP` | 10000 / 100 | Lực tối đa & bước tăng mỗi frame |
+| FPS | `FPS` | 120 | Tốc độ khung hình |
+
+### Engine vật lý (Pymunk)
+
+- **Không gian (Space)**: Quản lý toàn bộ thân vật lý, shape, và joint.
+- **Bi (Ball)**: `pymunk.Circle` gắn với `pymunk.Body` — có khối lượng, đàn hồi.
+- **Ma sát lăn**: Mô phỏng bằng `PivotJoint` với `max_force` giới hạn, `max_bias = 0`.
+- **Thành bàn (Cushion)**: `pymunk.Poly` tĩnh (`STATIC body`) — 6 polygon.
+- **Lỗ bi (Pocket)**: Kiểm tra khoảng cách Euclid (`math.dist`) giữa bi và tâm lỗ.
+
+### Các class & hàm chính
+
+| Thành phần | Loại | Mô tả |
+|------------|------|-------|
+| `Cue` | Class | Cơ bi-a — xoay theo hướng chuột, vẽ bằng `pygame.transform.rotate` |
+| `create_ball()` | Function | Tạo bi vật lý: Body + Circle + PivotJoint (ma sát) |
+| `create_cushion()` | Function | Tạo thành bàn tĩnh từ tọa độ polygon |
+| `draw_text()` | Function | Render text lên surface tại tọa độ (x, y) |
+| `main()` | Function | Hàm chính — khởi tạo, game loop, xử lý sự kiện |
+
+### Game Loop
+
+```
+┌─────────────────────────────────────────────┐
+│ clock.tick(FPS) → space.step(1/FPS) │
+│ │
+│ 1. Vẽ nền + bàn │
+│ 2. Kiểm tra bi vào lỗ (gom → xóa an toàn) │
+│ 3. Vẽ các bi │
+│ 4. Kiểm tra bi đã dừng → cho phép bắn │
+│ 5. Vẽ cơ + tính góc theo chuột │
+│ 6. Tích lực / bắn xung lực │
+│ 7. Vẽ panel dưới (bi đã vào lỗ + mạng) │
+│ 8. Kiểm tra thắng/thua │
+│ 9. Xử lý sự kiện (chuột, thoát) │
+│ 10. pygame.display.update() │
+└─────────────────────────────────────────────┘
+```
+
+## 📁 Cấu trúc dự án
+
```
+Pool Game/
+├── main.py # Source code chính
+├── requirements.txt # Thư viện phụ thuộc
+├── Pool Game.sln # Visual Studio Solution
+├── Pool Game.pyproj # Visual Studio Python Project
+├── assets/
+│ └── images/
+│ ├── table.png # Ảnh bàn bi-a
+│ ├── cue.png # Ảnh cơ bi-a
+│ ├── ball_1.png # Bi số 1
+│ ├── ... # Bi số 2–15
+│ └── ball_16.png # Bi trắng (cue ball)
+├── pic/
+│ └── 0.jpg # Ảnh demo cho README
+├── .github/
+│ ├── FUNDING.yml # Cấu hình sponsor
+│ └── workflows/
+│ └── deploy-pygame.yml # CI/CD build WebAssembly (Pygbag)
+├── .gitattributes # Git LFS tracking
+└── .gitignore # Ignore rules
+```
+
+## 🚀 Cài đặt & Chạy
+
+### Yêu cầu
+- Python 3.11+
+- pip
+
+### Cài đặt thư viện
+
+```bash
+pip install -r requirements.txt
+```
+
+### Chạy game
+
+```bash
+python main.py
+```
+
+## 🎮 Điều khiển
+
+| Thao tác | Hành động |
+|----------|-----------|
+| **Di chuột** | Xoay cơ bi-a theo hướng chuột |
+| **Giữ chuột trái** | Tích lực (thanh đỏ dao động) |
+| **Thả chuột trái** | Bắn bi trắng theo hướng cơ |
+| **Đóng cửa sổ** | Thoát game |
+
+## 📜 Luật chơi
+
+- Bạn có **3 mạng**. Mỗi lần bi trắng rơi vào lỗ sẽ mất 1 mạng.
+- Đánh hết **15 bi màu** vào lỗ để **thắng**.
+- Hết mạng → **GAME OVER**.
+
+## 💖 Sponsor
+
+- [GitHub Sponsors](https://github.com/sponsors/tynab)
+- [Patreon](https://patreon.com/yamiannephilim)
+- [PayPal](https://paypal.me/yamiannephilim)
diff --git a/main.py b/main.py
index 0c78fb8..e629ffd 100644
--- a/main.py
+++ b/main.py
@@ -1,9 +1,27 @@
+"""
+Pool Game — Trò chơi Bi-a 2D mô phỏng vật lý.
+
+Sử dụng Pygame để render đồ họa và Pymunk làm engine vật lý 2D (rigid-body).
+Người chơi điều khiển cơ bi-a bằng chuột, tích lực bằng cách giữ chuột,
+và cố gắng đánh hết tất cả các bi vào lỗ trước khi hết mạng.
+
+Thư viện:
+ - pygame: Render đồ họa, xử lý sự kiện, quản lý vòng lặp game.
+ - pymunk: Mô phỏng vật lý 2D (va chạm, ma sát, đàn hồi).
+"""
+
+import math
+import sys
+
import pygame
import pymunk
import pymunk.pygame_util
-import math
-# common
+# ============================================================================
+# HẰNG SỐ CẤU HÌNH
+# ============================================================================
+
+# --- Cửa sổ & giao diện ---
TITLE = "Pool Game"
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 678
@@ -11,149 +29,188 @@
BACKGROUND_COLOR = (50, 50, 50)
TEXT_COLOR = (255, 255, 255)
-# ball data
-MAX_BALL = 17
-BALL_MASS = 5
-BALL_ELASTICITY = 0.8
-BALL_DIAMETER = 36
-
-# wall data
-FRICTION = 1000
-CUSHION_ELASTICITY = 0.6
-POCKET_DIAMETER = 70
-
-# shooting data
-MAX_FORCE = 10000
-FORCE_STEP = 100
-
-# power bar
-BAR_WIDTH = 10
-BAR_HEIGHT = 20
-BAR_SENSTIVITY = 1000
-BAR_COLOR = (255, 0, 0)
-
-# create six pockets on table
-POCKETS = [(55, 63), (592, 48), (1134, 64), (55, 616), (592, 629), (1134, 616)]
+# --- Thông số bi ---
+BALL_COUNT = 16 # Tổng số bi (15 bi màu + 1 bi trắng)
+BALL_MASS = 5 # Khối lượng mỗi bi (đơn vị Pymunk)
+BALL_ELASTICITY = 0.8 # Hệ số đàn hồi khi va chạm
+BALL_DIAMETER = 36 # Đường kính bi (pixel)
+BALL_RADIUS = BALL_DIAMETER / 2 # Bán kính bi (pixel)
+
+# --- Thông số bàn (tường & lỗ) ---
+FRICTION = 1000 # Lực ma sát mô phỏng qua PivotJoint
+CUSHION_ELASTICITY = 0.6 # Hệ số đàn hồi của thành bàn
+POCKET_DIAMETER = 70 # Đường kính lỗ bi (pixel)
+
+# --- Thông số bắn ---
+MAX_FORCE = 10000 # Lực bắn tối đa
+FORCE_STEP = 100 # Bước tăng lực mỗi frame khi giữ chuột
+
+# --- Thanh lực (power bar) ---
+BAR_WIDTH = 10 # Chiều rộng mỗi ô thanh lực (pixel)
+BAR_HEIGHT = 20 # Chiều cao mỗi ô thanh lực (pixel)
+BAR_SENSITIVITY = 1000 # Số lực cần cho mỗi ô hiển thị
+BAR_COLOR = (255, 0, 0) # Màu thanh lực (đỏ)
+
+# --- Vị trí bi trắng ---
+CUE_BALL_X = 888 # Tọa độ X đặt bi trắng (bên phải bàn)
+
+# --- Vị trí xếp tam giác bi ---
+RACK_START_X = 250 # Tọa độ X bắt đầu xếp tam giác
+RACK_START_Y = 267 # Tọa độ Y bắt đầu xếp tam giác
+
+# --- Vị trí ngoài màn hình (khi bi rơi vào lỗ) ---
+OFFSCREEN_POS = (-444, -444)
+
+# --- Thanh lực: offset hiển thị so với bi trắng ---
+POWER_BAR_OFFSET_X = 70 # Khoảng cách ngang từ bi trắng đến thanh lực
+POWER_BAR_OFFSET_Y = 30 # Khoảng cách dọc từ bi trắng đến thanh lực
+POWER_BAR_SPACING = 15 # Khoảng cách giữa các ô thanh lực
+
+# --- Panel dưới: hiển thị bi đã vào lỗ ---
+POTTED_BALL_X_START = 10 # Tọa độ X bắt đầu vẽ bi đã vào lỗ
+POTTED_BALL_SPACING = 50 # Khoảng cách giữa các bi đã vào lỗ
+POTTED_BALL_Y_OFFSET = 10 # Khoảng cách dọc từ đường kẻ panel
+
+# --- Vị trí text ---
+LIVES_TEXT_X_OFFSET = 200 # Khoảng cách từ cạnh phải cho text "LIVES"
+LIVES_TEXT_Y_OFFSET = 10 # Khoảng cách dọc từ đường kẻ panel
+GAMEOVER_X_OFFSET = 160 # Offset X từ tâm cho text "GAME OVER"/"YOU WIN"
+GAMEOVER_Y_OFFSET = 100 # Offset Y từ tâm cho text "GAME OVER"/"YOU WIN"
+
+# --- Tọa độ 6 lỗ bi trên bàn (x, y) ---
+POCKETS = [
+ (55, 63), (592, 48), (1134, 64), # 3 lỗ phía trên
+ (55, 616), (592, 629), (1134, 616), # 3 lỗ phía dưới
+]
-# create pool table cushions
+# --- Tọa độ đỉnh của 6 thành bàn (polygon) ---
CUSHIONS = [
- [(88, 56), (109, 77), (555, 77), (564, 56)],
- [(621, 56), (630, 77), (1081, 77), (1102, 56)],
- [(89, 621), (110, 600), (556, 600), (564, 621)],
- [(622, 621), (630, 600), (1081, 600), (1102, 621)],
- [(56, 96), (77, 117), (77, 560), (56, 581)],
- [(1143, 96), (1122, 117), (1122, 560), (1143, 581)],
+ [(88, 56), (109, 77), (555, 77), (564, 56)], # Thành trên-trái
+ [(621, 56), (630, 77), (1081, 77), (1102, 56)], # Thành trên-phải
+ [(89, 621), (110, 600), (556, 600), (564, 621)], # Thành dưới-trái
+ [(622, 621), (630, 600), (1081, 600), (1102, 621)], # Thành dưới-phải
+ [(56, 96), (77, 117), (77, 560), (56, 581)], # Thành trái
+ [(1143, 96), (1122, 117), (1122, 560), (1143, 581)], # Thành phải
]
-# initilize the modules
-pygame.init()
+# --- Tốc độ khung hình ---
+FPS = 120
-# fonts
-font = pygame.font.SysFont("Lato", 30)
-large_font = pygame.font.SysFont("Lato", 60)
-# clock
-FPS = 120
-clock = pygame.time.Clock()
-
-# game window
-screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT + BOTTOM_PANEL))
-pygame.display.set_caption(TITLE)
-
-# pymunk space
-space = pymunk.Space()
-static_body = space.static_body
-draw_options = pymunk.pygame_util.DrawOptions(screen)
-
-# game variables
-lives = 3
-force = 0
-force_direction = 1
-game_running = True
-cue_ball_potted = False
-taking_shot = True
-powering_up = False
-potted_balls = []
-
-# load images
-cue_image = pygame.image.load("assets/images/cue.png").convert_alpha()
-table_image = pygame.image.load("assets/images/table.png").convert_alpha()
-ball_images = []
-for i in range(1, MAX_BALL):
- ball_image = pygame.image.load(f"assets/images/ball_{i}.png").convert_alpha()
- ball_images.append(ball_image)
-
-
-# function for outputting text onto the screen
-def draw_text(text, font, text_col, x, y):
- screen.blit(font.render(text, True, text_col), (x, y))
-
-
-# function for creating balls
-def create_ball(radius, pos):
+# ============================================================================
+# HÀM TIỆN ÍCH
+# ============================================================================
+
+
+def draw_text(
+ surface: pygame.Surface,
+ text: str,
+ font: pygame.font.Font,
+ color: tuple[int, int, int],
+ x: float,
+ y: float,
+) -> None:
+ """Vẽ chuỗi text lên bề mặt tại tọa độ (x, y).
+
+ Args:
+ surface: Bề mặt Pygame để vẽ lên.
+ text: Nội dung chuỗi cần hiển thị.
+ font: Font chữ sử dụng.
+ color: Màu chữ dạng RGB tuple.
+ x: Tọa độ X góc trên-trái của text.
+ y: Tọa độ Y góc trên-trái của text.
+ """
+ surface.blit(font.render(text, True, color), (x, y))
+
+
+def create_ball(
+ space: pymunk.Space,
+ static_body: pymunk.Body,
+ radius: float,
+ pos: tuple[float, float],
+) -> pymunk.Circle:
+ """Tạo một bi vật lý trong không gian Pymunk.
+
+ Mỗi bi gồm:
+ - Body: thân vật lý có vị trí và vận tốc.
+ - Circle shape: hình tròn với bán kính, khối lượng, đàn hồi.
+ - PivotJoint: khớp nối giả để mô phỏng ma sát lăn.
+
+ Args:
+ space: Không gian vật lý Pymunk.
+ static_body: Thân tĩnh của space, dùng làm neo cho PivotJoint.
+ radius: Bán kính bi (pixel).
+ pos: Vị trí ban đầu (x, y).
+
+ Returns:
+ Shape hình tròn (pymunk.Circle) đại diện cho bi.
+ """
body = pymunk.Body()
body.position = pos
shape = pymunk.Circle(body, radius)
shape.mass = BALL_MASS
shape.elasticity = BALL_ELASTICITY
- # use pivot joint to add friction
+ # Dùng PivotJoint để mô phỏng ma sát lăn trên mặt bàn
pivot = pymunk.PivotJoint(static_body, body, (0, 0), (0, 0))
- pivot.max_bias = 0 # disable joint correction
- pivot.max_force = FRICTION # emulate linear friction
+ pivot.max_bias = 0 # Tắt tự hiệu chỉnh vị trí của joint
+ pivot.max_force = FRICTION # Giới hạn lực = mô phỏng ma sát tuyến tính
space.add(body, shape, pivot)
return shape
-# setup game balls
-balls = []
-rows = 5
-
-# potting balls
-for col in range(5):
- for row in range(rows):
- balls.append(
- create_ball(
- BALL_DIAMETER / 2,
- (
- 250 + col * (BALL_DIAMETER + 1),
- 267 + row * (BALL_DIAMETER + 1) + col * BALL_DIAMETER / 2,
- ),
- )
- )
- rows -= 1
-
-# cue ball
-pos = (888, SCREEN_HEIGHT / 2)
-cue_ball = create_ball(BALL_DIAMETER / 2, pos)
-balls.append(cue_ball)
-
+def create_cushion(
+ space: pymunk.Space,
+ poly_dims: list[tuple[int, int]],
+) -> None:
+ """Tạo một thành bàn (cushion) tĩnh trong không gian vật lý.
-# function for creating cushions
-def create_cushion(poly_dims):
+ Args:
+ space: Không gian vật lý Pymunk.
+ poly_dims: Danh sách tọa độ đỉnh polygon của thành bàn.
+ """
body = pymunk.Body(body_type=pymunk.Body.STATIC)
- # body.position = (0, 0)
shape = pymunk.Poly(body, poly_dims)
shape.elasticity = CUSHION_ELASTICITY
space.add(body, shape)
-for cushion in CUSHIONS:
- create_cushion(cushion)
-
-
-# create pool cue
class Cue:
- def __init__(self, pos):
- self.original_image = cue_image
- self.angle = 0
+ """Cơ bi-a — hiển thị và xoay theo hướng chuột.
+
+ Attributes:
+ original_image: Ảnh gốc của cơ (không xoay).
+ angle: Góc xoay hiện tại (độ).
+ image: Ảnh đã xoay theo góc hiện tại.
+ rect: Hình chữ nhật bao quanh ảnh, dùng để định vị.
+ """
+
+ def __init__(self, image: pygame.Surface, pos: tuple[float, float]) -> None:
+ """Khởi tạo cơ bi-a tại vị trí cho trước.
+
+ Args:
+ image: Ảnh gốc của cơ bi-a.
+ pos: Vị trí tâm ban đầu (x, y).
+ """
+ self.original_image = image
+ self.angle = 0.0
self.image = pygame.transform.rotate(self.original_image, self.angle)
self.rect = self.image.get_rect()
self.rect.center = pos
- def update(self, angle):
+ def update(self, angle: float) -> None:
+ """Cập nhật góc xoay của cơ.
+
+ Args:
+ angle: Góc mới (độ), tính từ trục X dương ngược chiều kim đồng hồ.
+ """
self.angle = angle
- def draw(self, surface):
+ def draw(self, surface: pygame.Surface) -> None:
+ """Vẽ cơ bi-a lên bề mặt, xoay quanh tâm rect.
+
+ Args:
+ surface: Bề mặt Pygame để vẽ lên.
+ """
self.image = pygame.transform.rotate(self.original_image, self.angle)
surface.blit(
self.image,
@@ -164,150 +221,242 @@ def draw(self, surface):
)
-cue = Cue(balls[-1].body.position)
-
-# create power bars to show how hard the cue ball will be hit
-power_bar = pygame.Surface((BAR_WIDTH, BAR_HEIGHT))
-power_bar.fill(BAR_COLOR)
-
-# game loop
-game_on = True
-
-while game_on:
- clock.tick(FPS)
- space.step(1 / FPS)
-
- # fill background
- screen.fill(BACKGROUND_COLOR)
-
- # draw pool table
- screen.blit(table_image, (0, 0))
+# ============================================================================
+# HÀM CHÍNH
+# ============================================================================
+
+
+def main() -> None:
+ """Hàm chính — khởi tạo game và chạy vòng lặp chính.
+
+ Luồng thực thi:
+ 1. Khởi tạo Pygame, Pymunk, tải tài nguyên.
+ 2. Tạo các bi, thành bàn, cơ bi-a.
+ 3. Vòng lặp game: xử lý sự kiện → cập nhật vật lý → vẽ → hiển thị.
+ 4. Thoát khi người chơi đóng cửa sổ.
+ """
+ # --- Khởi tạo Pygame ---
+ pygame.init()
+ font = pygame.font.SysFont("Lato", 30)
+ large_font = pygame.font.SysFont("Lato", 60)
+ clock = pygame.time.Clock()
+
+ # --- Tạo cửa sổ game ---
+ screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT + BOTTOM_PANEL))
+ pygame.display.set_caption(TITLE)
+
+ # --- Khởi tạo không gian vật lý Pymunk ---
+ space = pymunk.Space()
+ static_body = space.static_body
+
+ # --- Biến trạng thái game ---
+ lives = 3 # Số mạng còn lại
+ force = 0 # Lực bắn hiện tại
+ force_direction = 1 # Hướng tăng/giảm lực (+1 tăng, -1 giảm)
+ game_running = True # Game đang chạy (chưa thua/thắng)
+ cue_ball_potted = False # Bi trắng vừa rơi vào lỗ
+ taking_shot = True # Đang ở trạng thái chờ bắn
+ powering_up = False # Đang giữ chuột tích lực
+ potted_balls = [] # Danh sách ảnh bi đã vào lỗ (hiển thị ở panel dưới)
+ cue_angle = 0.0 # Góc cơ bi-a (khởi tạo tránh lỗi tham chiếu)
+
+ # --- Tải hình ảnh ---
+ cue_image = pygame.image.load("assets/images/cue.png").convert_alpha()
+ table_image = pygame.image.load("assets/images/table.png").convert_alpha()
+ ball_images = [
+ pygame.image.load(f"assets/images/ball_{i}.png").convert_alpha()
+ for i in range(1, BALL_COUNT + 1)
+ ]
+
+ # --- Tạo các bi trên bàn ---
+ balls = []
+ rows = 5
+ # Xếp 15 bi màu theo hình tam giác (5 cột, số hàng giảm dần)
+ for col in range(5):
+ for row in range(rows):
+ pos = (
+ RACK_START_X + col * (BALL_DIAMETER + 1),
+ RACK_START_Y + row * (BALL_DIAMETER + 1) + col * BALL_RADIUS,
+ )
+ balls.append(create_ball(space, static_body, BALL_RADIUS, pos))
+ rows -= 1
+
+ # Bi trắng (luôn ở cuối danh sách balls)
+ cue_ball_pos = (CUE_BALL_X, SCREEN_HEIGHT / 2)
+ cue_ball = create_ball(space, static_body, BALL_RADIUS, cue_ball_pos)
+ balls.append(cue_ball)
+
+ # --- Tạo 6 thành bàn ---
+ for cushion_vertices in CUSHIONS:
+ create_cushion(space, cushion_vertices)
+
+ # --- Tạo cơ bi-a ---
+ cue = Cue(cue_image, balls[-1].body.position)
+
+ # --- Tạo thanh lực (power bar) ---
+ power_bar = pygame.Surface((BAR_WIDTH, BAR_HEIGHT))
+ power_bar.fill(BAR_COLOR)
+
+ # ========================================================================
+ # VÒNG LẶP CHÍNH
+ # ========================================================================
+ game_on = True
+
+ while game_on:
+ clock.tick(FPS)
+ space.step(1 / FPS)
+
+ # --- Vẽ nền và bàn bi-a ---
+ screen.fill(BACKGROUND_COLOR)
+ screen.blit(table_image, (0, 0))
+
+ # --- Kiểm tra bi rơi vào lỗ ---
+ # Gom các bi cần xóa để tránh lỗi khi sửa list đang duyệt
+ balls_to_remove = []
+ for i, ball in enumerate(balls):
+ for pocket in POCKETS:
+ ball_pos = ball.body.position
+ distance = math.dist((ball_pos[0], ball_pos[1]), pocket)
+ if distance <= POCKET_DIAMETER / 2:
+ if i == len(balls) - 1:
+ # Bi trắng rơi vào lỗ → mất mạng, đặt ra ngoài tạm
+ lives -= 1
+ cue_ball_potted = True
+ ball.body.position = OFFSCREEN_POS
+ ball.body.velocity = (0.0, 0.0)
+ else:
+ # Bi màu rơi vào lỗ → đánh dấu cần xóa
+ balls_to_remove.append(i)
+ ball.body.position = OFFSCREEN_POS
+ ball.body.velocity = (0.0, 0.0)
+ break # Mỗi bi chỉ rơi vào 1 lỗ
+
+ # Xóa bi từ index lớn → nhỏ để không bị lệch index
+ for idx in sorted(balls_to_remove, reverse=True):
+ removed_ball = balls.pop(idx)
+ space.remove(removed_ball.body)
+ potted_balls.append(ball_images.pop(idx))
+
+ # --- Vẽ các bi trên bàn ---
+ for i, ball in enumerate(balls):
+ screen.blit(
+ ball_images[i],
+ (ball.body.position[0] - ball.radius, ball.body.position[1] - ball.radius),
+ )
- # check if any balls have been potted
- for i, ball in enumerate(balls):
- for pocket in POCKETS:
- if (
- math.sqrt(
- (abs(ball.body.position[0] - pocket[0]) ** 2)
- + (abs(ball.body.position[1] - pocket[1]) ** 2)
+ # --- Kiểm tra tất cả bi đã dừng chưa ---
+ taking_shot = True
+ for ball in balls:
+ if int(ball.body.velocity[0]) != 0 or int(ball.body.velocity[1]) != 0:
+ taking_shot = False
+ break
+
+ # --- Vẽ cơ bi-a khi đang chờ bắn ---
+ if taking_shot and game_running:
+ if cue_ball_potted:
+ # Đặt lại bi trắng về vị trí ban đầu
+ balls[-1].body.position = (CUE_BALL_X, SCREEN_HEIGHT / 2)
+ cue_ball_potted = False
+ # Tính góc cơ theo hướng chuột
+ mouse_pos = pygame.mouse.get_pos()
+ cue.rect.center = balls[-1].body.position
+ cue_ball_pos_current = balls[-1].body.position
+ cue_angle = math.degrees(
+ math.atan2(
+ -(cue_ball_pos_current[1] - mouse_pos[1]),
+ cue_ball_pos_current[0] - mouse_pos[0],
)
- <= POCKET_DIAMETER / 2
- ):
- ball.body.position = (-444, -444)
- ball.body.velocity = (0.0, 0.0)
- # check if the potted ball was the cue ball
- if i == len(balls) - 1:
- lives -= 1
- cue_ball_potted = True
- else:
- space.remove(ball.body)
- balls.remove(ball)
- potted_balls.append(ball_images[i])
- ball_images.pop(i)
-
- # draw pool balls
- for i, ball in enumerate(balls):
- screen.blit(
- ball_images[i],
- (ball.body.position[0] - ball.radius, ball.body.position[1] - ball.radius),
- )
-
- taking_shot = True
-
- # check if all the balls have stopped moving
- for ball in balls:
- if int(ball.body.velocity[0]) != 0 or int(ball.body.velocity[1]) != 0:
- taking_shot = False
-
- # draw pool cue
- if taking_shot and game_running:
- if cue_ball_potted:
- # reposition cue ball
- balls[-1].body.position = (888, SCREEN_HEIGHT / 2)
- cue_ball_potted = False
- # calculate pool cue angle
- mouse_pos = pygame.mouse.get_pos()
- cue.rect.center = balls[-1].body.position
- cue_angle = math.degrees(
- math.atan2(
- -(balls[-1].body.position[1] - mouse_pos[1]),
- balls[-1].body.position[0] - mouse_pos[0],
)
+ cue.update(cue_angle)
+ cue.draw(screen)
+
+ # --- Tích lực hoặc bắn ---
+ if powering_up and game_running:
+ # Đang giữ chuột → tăng/giảm lực theo hướng dao động
+ force += FORCE_STEP * force_direction
+ if force >= MAX_FORCE or force <= 0:
+ force_direction *= -1
+ # Vẽ các ô thanh lực
+ for bar_idx in range(math.ceil(force / BAR_SENSITIVITY)):
+ screen.blit(
+ power_bar,
+ (
+ balls[-1].body.position[0] - POWER_BAR_OFFSET_X + bar_idx * POWER_BAR_SPACING,
+ balls[-1].body.position[1] + POWER_BAR_OFFSET_Y,
+ ),
+ )
+ elif not powering_up and taking_shot:
+ # Vừa thả chuột → áp dụng xung lực lên bi trắng
+ impulse_x = force * -math.cos(math.radians(cue_angle))
+ impulse_y = force * math.sin(math.radians(cue_angle))
+ balls[-1].body.apply_impulse_at_local_point((impulse_x, impulse_y), (0, 0))
+ force = 0
+ force_direction = 1
+
+ # --- Vẽ panel dưới ---
+ pygame.draw.rect(
+ screen, BACKGROUND_COLOR, (0, SCREEN_HEIGHT, SCREEN_WIDTH, BOTTOM_PANEL)
)
- cue.update(cue_angle)
- cue.draw(screen)
-
- # power up pool cue
- if powering_up and game_running:
- force += FORCE_STEP * force_direction
- if force >= MAX_FORCE or force <= 0:
- force_direction *= -1
- # draw power bars
- for adjustment in range(math.ceil(force / BAR_SENSTIVITY)):
+
+ # Vẽ các bi đã vào lỗ ở panel dưới
+ for i, ball_img in enumerate(potted_balls):
screen.blit(
- power_bar,
- (
- balls[-1].body.position[0] - 70 + adjustment * 15,
- balls[-1].body.position[1] + 30,
- ),
+ ball_img,
+ (POTTED_BALL_X_START + i * POTTED_BALL_SPACING, SCREEN_HEIGHT + POTTED_BALL_Y_OFFSET),
)
- elif not powering_up and taking_shot:
- balls[-1].body.apply_impulse_at_local_point(
- (
- force * -math.cos(math.radians(cue_angle)),
- force * math.sin(math.radians(cue_angle)),
- ),
- (0, 0),
- )
- force = 0
- force_direction = 1
-
- # draw bottom panel
- pygame.draw.rect(
- screen, BACKGROUND_COLOR, (0, SCREEN_HEIGHT, SCREEN_WIDTH, BOTTOM_PANEL)
- )
-
- # draw potted balls in bottom panel
- for i, ball in enumerate(potted_balls):
- screen.blit(ball, (10 + (i * 50), SCREEN_HEIGHT + 10))
- # draw lives
- draw_text(
- f"LIVES: {str(lives)}", font, TEXT_COLOR, SCREEN_WIDTH - 200, SCREEN_HEIGHT + 10
- )
-
- # check for game over
- if lives <= 0:
+ # Hiển thị số mạng còn lại
draw_text(
- "GAME OVER",
- large_font,
+ screen,
+ f"LIVES: {lives}",
+ font,
TEXT_COLOR,
- SCREEN_WIDTH / 2 - 160,
- SCREEN_HEIGHT / 2 - 100,
+ SCREEN_WIDTH - LIVES_TEXT_X_OFFSET,
+ SCREEN_HEIGHT + LIVES_TEXT_Y_OFFSET,
)
- game_running = False
- # check if all balls are potted
- if len(balls) == 1:
- draw_text(
- "YOU WIN",
- large_font,
- TEXT_COLOR,
- SCREEN_WIDTH / 2 - 160,
- SCREEN_HEIGHT / 2 - 100,
- )
- game_running = False
+ # --- Kiểm tra thua ---
+ if lives <= 0:
+ draw_text(
+ screen,
+ "GAME OVER",
+ large_font,
+ TEXT_COLOR,
+ SCREEN_WIDTH / 2 - GAMEOVER_X_OFFSET,
+ SCREEN_HEIGHT / 2 - GAMEOVER_Y_OFFSET,
+ )
+ game_running = False
+
+ # --- Kiểm tra thắng (chỉ còn bi trắng) ---
+ if len(balls) == 1:
+ draw_text(
+ screen,
+ "YOU WIN",
+ large_font,
+ TEXT_COLOR,
+ SCREEN_WIDTH / 2 - GAMEOVER_X_OFFSET,
+ SCREEN_HEIGHT / 2 - GAMEOVER_Y_OFFSET,
+ )
+ game_running = False
+
+ # --- Xử lý sự kiện ---
+ for event in pygame.event.get():
+ if event.type == pygame.MOUSEBUTTONDOWN and taking_shot:
+ powering_up = True
+ if event.type == pygame.MOUSEBUTTONUP and taking_shot:
+ powering_up = False
+ if event.type == pygame.QUIT:
+ game_on = False
+
+ pygame.display.update()
+
+ pygame.quit()
+ sys.exit()
- # event handler
- for event in pygame.event.get():
- if event.type == pygame.MOUSEBUTTONDOWN and taking_shot:
- powering_up = True
- if event.type == pygame.MOUSEBUTTONUP and taking_shot:
- powering_up = False
- if event.type == pygame.QUIT:
- game_on = False
- # space.debug_draw(draw_options)
- pygame.display.update()
+# ============================================================================
+# ĐIỂM VÀO CHƯƠNG TRÌNH
+# ============================================================================
-pygame.quit()
+if __name__ == "__main__":
+ main()