diff --git a/common/game/billiards/colorBall.py b/common/game/billiards/colorBall.py new file mode 100644 index 0000000..a909ace --- /dev/null +++ b/common/game/billiards/colorBall.py @@ -0,0 +1,13 @@ +from common.game.billiards.baseBilliard import BaseBilliard +from enums.color import WHITE +from enums.category import CUE_BALL + + +class ColorBall(BaseBilliard): + def __init__(self, score: int, category: int, color: tuple[int, int, int], pos=(0, 0)): + super().__init__(score, category, color, pos) + self.__start_pos = pos + + def reset(self): + self.pos = self.__start_pos + self.activity = True diff --git a/common/game/billiards/cueBall.py b/common/game/billiards/cueBall.py new file mode 100644 index 0000000..182d40d --- /dev/null +++ b/common/game/billiards/cueBall.py @@ -0,0 +1,8 @@ +from common.game.billiards.baseBilliard import BaseBilliard +from enums.color import WHITE +from enums.category import CUE_BALL + + +class CueBall(BaseBilliard): + def __init__(self, pos=(0, 0)): + super().__init__(-1, CUE_BALL, WHITE, pos) diff --git a/common/game/billiards/fakeCueball.py b/common/game/billiards/fakeCueball.py new file mode 100644 index 0000000..651b2c7 --- /dev/null +++ b/common/game/billiards/fakeCueball.py @@ -0,0 +1,15 @@ +import pygame + +from common.drawableItem import DrawableItem +from config.sys import BALL_RADIUS +from enums.color import WHITE, BLACK + + +class FakeCueBall(DrawableItem): + def __init__(self, pos): + self.pos = pos + + def draw(self, window: pygame.Surface): + pygame.draw.circle(window, WHITE, self.pos, BALL_RADIUS) + pygame.draw.circle(window, BLACK, self.pos, BALL_RADIUS, 1) + pass diff --git a/common/game/billiards/redBall.py b/common/game/billiards/redBall.py new file mode 100644 index 0000000..c5de051 --- /dev/null +++ b/common/game/billiards/redBall.py @@ -0,0 +1,11 @@ +from common.game.billiards.baseBilliard import BaseBilliard +from enums.color import RED +from enums.category import RED_BALL + + +class RedBall(BaseBilliard): + def __init__(self, pos): + super().__init__(1, RED_BALL, RED, pos) + + def reset(self): + pass diff --git a/common/game/game.py b/common/game/game.py new file mode 100644 index 0000000..8093798 --- /dev/null +++ b/common/game/game.py @@ -0,0 +1,167 @@ +import sys + +import pygame +import pymunk + +from common.game.billiards.baseBilliard import BaseBilliard +from common.game.billiards.colorBall import ColorBall +from common.game.billiards.cueBall import CueBall +from common.game.billiards.fakeCueball import FakeCueBall +from common.game.billiards.redBall import RedBall +from common.game.cue import Cue +from common.game.gameData import GameData +from common.game.ruleManager import RuleManager +from common.game.table import Table +from config.sys import * +from enums.color import * +from enums.category import * + + +class Game(object): + def __init__(self): + self.__game_data = GameData() + self.__table = Table(self.__game_data) + self.__cue_ball = CueBall((TABLE_WIDTH * 7 / 8, TABLE_HEIGHT / 4)) + self.__game_data.balls.append(self.__cue_ball) + self.__cue = Cue(self.__cue_ball, self.__game_data) + self.__space = pymunk.Space() + self.__clock = pygame.time.Clock() + self.__space.gravity = (0, 0) + self.__rule = RuleManager(self.__game_data) + self.__hitting = False + self.__window = pygame.display.set_mode(SCREEN_SIZE) + self.__fake_ball = FakeCueBall((99999, 99999)) + pass + + def add_ball(self, ball: BaseBilliard): + self.__game_data.balls.append(ball) + self.__space.add(ball.rigid_body, ball.shape) + + def init_table(self): + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS, TABLE_HEIGHT / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 2, TABLE_HEIGHT / 2 - BALL_RADIUS / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 2, TABLE_HEIGHT / 2 + BALL_RADIUS / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 3, TABLE_HEIGHT / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 3, TABLE_HEIGHT / 2 - BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 3, TABLE_HEIGHT / 2 + BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 4, TABLE_HEIGHT / 2 - BALL_RADIUS / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 4, TABLE_HEIGHT / 2 + BALL_RADIUS / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 4, TABLE_HEIGHT / 2 - BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 4, TABLE_HEIGHT / 2 + BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 5, TABLE_HEIGHT / 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 5, TABLE_HEIGHT / 2 - BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 5, TABLE_HEIGHT / 2 + BALL_RADIUS))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 5, TABLE_HEIGHT / 2 - BALL_RADIUS * 2))) + self.add_ball(RedBall((TABLE_WIDTH / 4 - BALL_RADIUS * 5, TABLE_HEIGHT / 2 + BALL_RADIUS * 2))) + self.add_ball(ColorBall(2, YELLOW_BALL, YELLOW, (TABLE_WIDTH * 3 / 4, TABLE_HEIGHT / 3))) + self.add_ball(ColorBall(3, COFFEE_BALL, COFFEE, (TABLE_WIDTH * 3 / 4, TABLE_HEIGHT / 2))) + self.add_ball(ColorBall(4, CLAN_BALL, CLAN, (TABLE_WIDTH * 3 / 4, TABLE_HEIGHT * 2 / 3))) + self.add_ball(ColorBall(5, BLUE_BALL, BLUE, (TABLE_WIDTH / 2, TABLE_HEIGHT / 2))) + self.add_ball(ColorBall(6, PINK_BALL, PINK, (TABLE_WIDTH / 4 + BALL_RADIUS * 3, TABLE_HEIGHT / 2))) + self.add_ball(ColorBall(7, BLACK_BALL, BLACK, (TABLE_WIDTH / 8, TABLE_HEIGHT / 2))) + pass + + def print_score(self, left_score, right_score): + font = pygame.font.Font("./src/fonts/PingFang.ttc", 24) + left_surface = font.render("left: " + str(left_score), True, BLACK, None) + right_surface = font.render("right: " + str(right_score), True, BLACK, None) + round_surface = font.render("round: " + ("left" if self.__rule.round == 1 else "right"), True, BLACK, None) + self.__window.blit(left_surface, (SCREEN_WIDTH / 8, SCREEN_HEIGHT * 7 / 8)) + self.__window.blit(right_surface, (SCREEN_WIDTH * 7 / 8 - right_surface.get_width(), SCREEN_HEIGHT * 7 / 8)) + self.__window.blit(round_surface, (SCREEN_WIDTH / 2 - round_surface.get_width() / 2, SCREEN_HEIGHT * 7 / 8)) + + def hit(self, x, y): + self.__hitting = True + self.__game_data.last_goal_balls = [_ for _ in self.__game_data.goal_balls] + self.__rule.get_should_hit() + self.__game_data.collide_log.clear() + self.__game_data.goal_balls.clear() + pos_x = x - (SCREEN_WIDTH - TABLE_WIDTH) / 2 + pos_y = y - (SCREEN_HEIGHT - TABLE_HEIGHT) / 2 + self.__cue_ball.rigid_body.velocity = pymunk.Vec2d( + pos_x - self.__game_data.balls[0].rigid_body.position.x, + pos_y - self.__game_data.balls[0].rigid_body.position.y).scale_to_length(512) + + def start(self): + def new_ball(space, pos): + ball = RedBall(pos) + body = ball.rigid_body + shape = ball.shape + space.add(body, shape) + self.__game_data.balls.append(ball) + return shape + + def post_collision_ball_ball(arbiter, space, data): + self.__game_data.collide_log.append(arbiter.shapes[0].collision_type) + + pygame.init() + pygame.display.set_caption(TITLE) + + self.init_table() + + for _ in range(1, 9): + self.__space.add_collision_handler(_, CUE_BALL).post_solve = post_collision_ball_ball + + for _ in range(6): + self.__space.add(self.__table.cushing_body[_], self.__table.cushing_shape[_]) + self.__space.add(self.__game_data.balls[0].rigid_body, self.__game_data.balls[0].shape) + + force = 5 + add_force = False + + while True: + if self.__rule.settlement: + self.__rule.judge() + if self.__game_data.is_stable() and self.__hitting: + self.__rule.settlement = True + self.__hitting = False + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + exit() + if not self.__game_data.is_stable(): + continue + if event.type == pygame.MOUSEMOTION: + self.__cue.mouse_pos = event.pos + if self.__rule.need_reset_cue_ball: + self.__fake_ball.pos = event.pos + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + add_force = True + if event.type == pygame.MOUSEBUTTONUP: + if event.button == 1: + if self.__rule.need_reset_cue_ball: + pos_x = - (SCREEN_WIDTH - TABLE_WIDTH) / 2 + event.pos[0] + pos_y = - (SCREEN_HEIGHT - TABLE_HEIGHT) / 2 + event.pos[1] + self.__cue_ball.pos = (pos_x + 1, pos_y + 1) + self.__fake_ball.pos = (99999, 99999) + self.__rule.need_reset_cue_ball = False + else: + self.hit(event.pos[0], event.pos[1]) + add_force = False + force = 5 + if event.button == 3: + pos_x = event.pos[0] - (SCREEN_WIDTH - TABLE_WIDTH) / 2 + pos_y = event.pos[1] - (SCREEN_HEIGHT - TABLE_HEIGHT) / 2 + new_ball(self.__space, (pos_x, pos_y)) + if add_force: + force += 5 + if force >= 512: + force = 512 + self.__window.fill(WHITE) + self.__table.goal(self.__space, self.__game_data.balls) + self.__table.draw(self.__window) + self.__fake_ball.draw(self.__window) + if not self.__rule.need_reset_cue_ball: + self.__cue.draw(self.__window) + for _ in self.__game_data.balls: + _.resistance() + _.draw(self.__window) + self.print_score(self.__game_data.left_score, self.__game_data.right_score) + self.__space.step(1 / FPS) + pygame.display.update() + self.__clock.tick(FPS) + pass + + pass diff --git a/common/game/gameData.py b/common/game/gameData.py new file mode 100644 index 0000000..bcf1760 --- /dev/null +++ b/common/game/gameData.py @@ -0,0 +1,24 @@ +from common.game.billiards.colorBall import ColorBall +from common.game.billiards.redBall import RedBall +from common.game.billiards.cueBall import CueBall + + +class GameData(object): + def __init__(self): + self.left_score = 0 + self.right_score = 0 + self.balls: list[ColorBall | RedBall | CueBall] = [] + self.last_balls: list[ColorBall | RedBall | CueBall] = [] + self.goal_balls: list[int] = [] + self.last_goal_balls: list[int] = [] + self.collide_log: list[int] = [] + pass + + def collide_ball(self, ball): + self.collide_log.append(ball) + + def is_stable(self) -> bool: + for _ in self.balls: + if not _.is_stable(): + return False + return True diff --git a/common/game/ruleManager.py b/common/game/ruleManager.py new file mode 100644 index 0000000..152dee2 --- /dev/null +++ b/common/game/ruleManager.py @@ -0,0 +1,124 @@ +from common.game.gameData import GameData +from enums.category import * + + +class RuleManager(object): + def __init__(self, game_data: GameData): + self.__game_data = game_data + self.__should_hit = [] + self.need_reset_cue_ball = False + self.settlement = False + self.round = 1 + + def judge(self): + self.print_log() + self.do_reset() + self.cal_score() + self.try_turn_round() + self.settlement = False + + def print_log(self): + print('round:' + str(self.round)) + print('collide: ' + str(self.__game_data.collide_log)) + print('collision_flag: ' + str(self.collision_legal())) + print('goal: ' + str(self.__game_data.goal_balls)) + print('goal_flag: ' + str(self.goal_legal())) + + def check_legality(self): + return self.collision_legal() and self.goal_legal() and self.cue_legal() + + def do_reset(self): + if not self.goal_legal(): + for _ in self.__game_data.balls: + if _.category in COLORED_BALLS and _.category in self.__game_data.goal_balls: + _.reset() + if not self.cue_legal(): + self.need_reset_cue_ball = True + for _ in self.__game_data.balls: + if _.category == RED_BALL and _.activity: + for __ in self.__game_data.balls: + if __.category in COLORED_BALLS and __.category in self.__game_data.goal_balls: + __.reset() + break + + def get_legality(self): + legality = [] + if not self.collision_legal(): + legality.append(legality) + if not self.goal_legal(): + legality.append(legality) + if not self.cue_legal(): + legality.append(legality) + return legality + + def try_turn_round(self): + if len(self.__game_data.goal_balls) != 0 and self.check_legality(): + return + self.__game_data.goal_balls.clear() + self.__game_data.last_goal_balls.clear() + self.round *= -1 + + def get_should_hit(self): + self.__should_hit.clear() + have_red_ball = False + for _ in self.__game_data.balls: + if _.category == RED_BALL and _.activity: + have_red_ball = True + break + if have_red_ball: + if len(self.__game_data.last_goal_balls) == 0: + return self.__should_hit.append(RED_BALL) + if self.__game_data.last_goal_balls[0] in COLORED_BALLS: + return self.__should_hit.append(RED_BALL) + self.__should_hit = [_ for _ in COLORED_BALLS] + return + should_hit = 7 + for _ in self.__game_data.balls: + if _.category < should_hit and _.activity: + should_hit = _.category + return self.__should_hit.append(should_hit) + + def collision_legal(self): + if len(self.__game_data.collide_log) == 0: + return False + if self.__game_data.collide_log[0] in self.__should_hit: + return True + return False + + def goal_legal(self): + without_cue_list = [_ for _ in self.__game_data.goal_balls] + if CUE_BALL in without_cue_list: + without_cue_list.remove(CUE_BALL) + if len(without_cue_list) == 0: + return True + if len(list(set(without_cue_list) & set(self.__should_hit))) == 0: + return False + if RED_BALL not in self.__should_hit and len(list(set(without_cue_list) & set(self.__should_hit))) != 1: + return True + return True + + def cue_legal(self): + return CUE_BALL not in self.__game_data.goal_balls + + def cal_score(self): + if self.round == 1: + if self.check_legality(): + for _ in self.__game_data.goal_balls: + self.__game_data.left_score += _ + return + if len(self.__game_data.collide_log) == 0: + self.__game_data.right_score += 4 + return + first_collide = self.__game_data.collide_log[0] + self.__game_data.right_score += first_collide if first_collide > 4 else 4 + else: + if self.check_legality(): + for _ in self.__game_data.goal_balls: + self.__game_data.right_score += _ + return + if len(self.__game_data.collide_log) == 0: + self.__game_data.left_score += 4 + return + first_collide = self.__game_data.collide_log[0] + self.__game_data.left_score += first_collide if first_collide > 4 else 4 + pass diff --git a/enums/rule.py b/enums/rule.py new file mode 100644 index 0000000..0a53b67 --- /dev/null +++ b/enums/rule.py @@ -0,0 +1,2 @@ +ILLEGAL_COLLISION = 1 +ILLEGAL_GOAL = 2 diff --git a/src/fonts/PingFang.ttc b/src/fonts/PingFang.ttc new file mode 100644 index 0000000..68add37 Binary files /dev/null and b/src/fonts/PingFang.ttc differ