diff --git a/README.md b/README.md index 6b315a5..f35889b 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,17 @@ ```python # main.py -game = EasyGame(time_to_play=1000, total_point_count=10, score=5, color="FF9800") +game = EasyGame( + time_to_play, score_to_pass, green_food_count, red_food_count, + playground_size: list,level: int = -1) ``` - `time_to_play`:遊戲執行的終止時間,單位是 frame,也就是遊戲內部更新畫面的次數,每更新一次 frame +1 -- `total_point_count`:遊戲中食物出現的最大數量。 -- `score`:遊戲通關的點數,要超過這個分數才算過關。 -- `color`:主角方塊的顏色,使用16進位顏色表示法 +- `green_food_count`:遊戲中綠色食物的數量。 +- `red_food_count`:遊戲中紅色食物的數量。 +- `score_to_pass`:遊戲通關的點數,要超過這個分數才算過關。 +- `playground_size`:可移動區域的大小。 使用逗號將數字隔開 `width,height` `100,200` +- `level`: 選定內建關卡,請注意,使用此設定將會覆蓋掉其他設定,預設為 -1 不選擇任何關卡。 ## 玩法 @@ -53,7 +57,7 @@ game = EasyGame(time_to_play=1000, total_point_count=10, score=5, color="FF9800" 2. 座標系統 - 螢幕大小 800 x 600 - - 主角方塊 50 x 50 + - 主角方塊 30 x 30 - 食物方塊 8 x 8 --- @@ -64,7 +68,7 @@ game = EasyGame(time_to_play=1000, total_point_count=10, score=5, color="FF9800" ```bash # 在easy game中,打開終端機 -python -m mlgame -i ./ml/ml_play_template.py ./ --time_to_play 1200 --total_point_count 15 --score 10 --color FF9800 +python -m mlgame -i ./ml/ml_play_template.py ./ --time_to_play 1200 --green_food_count 15 --red_food_count 10 --score_to_pass 10 --playground_size 100,200 ``` ## AI範例 @@ -163,5 +167,6 @@ class MLPlay: - `player`:玩家編號 - `score`:吃到的食物總數 - `rank`:排名 + - `passed`:是否通關 --- \ No newline at end of file diff --git a/blockly.json b/blockly.json index 209b6bf..922740c 100644 --- a/blockly.json +++ b/blockly.json @@ -20,8 +20,8 @@ [800, "right boundary", "右邊界"], [0, "top boundary", "上邊界"], [600, "bottom boundary", "下邊界"], - [50, "ball width", "球身的寬度"], - [50, "ball height", "球身的高度"], + [30, "ball width", "球身的寬度"], + [30, "ball height", "球身的高度"], [8, "food width", "食物的寬度"], [8, "food height", "食物的高度"] ], diff --git a/game_config.json b/game_config.json index a1adc48..bd1b77b 100644 --- a/game_config.json +++ b/game_config.json @@ -1,6 +1,6 @@ { "game_name": "easy_game", - "version": "2.1.0", + "version": "2.2.0-beta", "url": "https://github.com/PAIA-Playful-AI-Arena/easy_game", "description": "這是一個吃東西小遊戲,除了讓你熟習所有基本操作,也是 PAIA 的遊戲教學範例", "logo": [ @@ -8,7 +8,8 @@ "https://raw.githubusercontent.com/PAIA-Playful-AI-Arena/Paia-Desktop/master/media/easygame.svg" ], "user_num": { - "min": 1,"max": 1 + "min": 1, + "max": 1 }, "game_params": [ { @@ -21,15 +22,45 @@ "help": "set the limit of frame count , actually time will be revised according to your FPS ." }, { - "name": "total_point_count", - "verbose": "總食物數量", + "name": "green_food_count", + "verbose": "綠色食物數量", "type": "int", - "choices":[ 5 ,10 ,15,20,25,30,35,40,45,50], + "choices": [ + 5, + 10, + 15, + 20, + 25, + 30, + 35, + 40, + 45, + 50 + ], "help": "set the total number of points", "default": 10 }, { - "name": "score", + "name": "red_food_count", + "verbose": "紅色食物數量", + "type": "int", + "choices": [ + 5, + 10, + 15, + 20, + 25, + 30, + 35, + 40, + 45, + 50 + ], + "help": "set the total number of points", + "default": 10 + }, + { + "name": "score_to_pass", "verbose": "通關分數", "type": "int", "min": 1, @@ -38,25 +69,20 @@ "help": "set the score to win this game " }, { - "name": "color", - "verbose": "矩形顏色", - "type": "str", - "choices": [ - { - "verbose": "CYAN", - "value": "00BCD4" - }, - { - "verbose": "YELLOW", - "value": "FFEB3B" - }, - { - "verbose": "ORANGE", - "value": "FF9800" - } - ], - "help": "set the color of rectangle", - "default": "00BCD4" + "name": "playground_size", + "verbose": "場地尺寸", + "type": "list", + "default": "400,400", + "help": "set the size of playground " + }, + { + "name": "level", + "verbose": "內建關卡編號", + "type": "int", + "min": -1, + "max": 5, + "default": "-1", + "help": "選定內建關卡,請注意,使用此設定將會覆蓋掉其他設定,預設為 -1 不選擇任何關卡。" } ] } \ No newline at end of file diff --git a/levels/001.json b/levels/001.json new file mode 100644 index 0000000..a948b2c --- /dev/null +++ b/levels/001.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 300, + "playground_size": [ + 100, + 200 + ], + "score_to_pass": 10, + "green_food_count": 3, + "black_food_count": 0 +} \ No newline at end of file diff --git a/levels/002.json b/levels/002.json new file mode 100644 index 0000000..bb34c84 --- /dev/null +++ b/levels/002.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 300, + "playground_size": [ + 200, + 200 + ], + "score_to_pass": 15, + "green_food_count": 5, + "black_food_count": 0 +} \ No newline at end of file diff --git a/levels/003.json b/levels/003.json new file mode 100644 index 0000000..af6dd30 --- /dev/null +++ b/levels/003.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 500, + "playground_size": [ + 300, + 300 + ], + "score_to_pass": 15, + "green_food_count": 10, + "black_food_count": 0 +} \ No newline at end of file diff --git a/levels/004.json b/levels/004.json new file mode 100644 index 0000000..34d2278 --- /dev/null +++ b/levels/004.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 600, + "playground_size": [ + 300, + 300 + ], + "score_to_pass": 15, + "green_food_count": 7, + "black_food_count": 3 +} \ No newline at end of file diff --git a/levels/005.json b/levels/005.json new file mode 100644 index 0000000..cee2f71 --- /dev/null +++ b/levels/005.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 800, + "playground_size": [ + 300, + 300 + ], + "score_to_pass": 15, + "green_food_count": 7, + "black_food_count": 7 +} \ No newline at end of file diff --git a/levels/006.json b/levels/006.json new file mode 100644 index 0000000..ef06d11 --- /dev/null +++ b/levels/006.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1000, + "playground_size": [ + 400, + 400 + ], + "score_to_pass": 15, + "green_food_count": 7, + "black_food_count": 10 +} \ No newline at end of file diff --git a/levels/007.json b/levels/007.json new file mode 100644 index 0000000..49c8e26 --- /dev/null +++ b/levels/007.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1000, + "playground_size": [ + 400, + 400 + ], + "score_to_pass": 20, + "green_food_count": 7, + "black_food_count": 13 +} \ No newline at end of file diff --git a/levels/008.json b/levels/008.json new file mode 100644 index 0000000..61a8a29 --- /dev/null +++ b/levels/008.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1200, + "playground_size": [ + 500, + 500 + ], + "score_to_pass": 20, + "green_food_count": 10, + "black_food_count": 15 +} \ No newline at end of file diff --git a/levels/009.json b/levels/009.json new file mode 100644 index 0000000..bda3f34 --- /dev/null +++ b/levels/009.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1200, + "playground_size": [ + 600, + 500 + ], + "score_to_pass": 25, + "green_food_count": 15, + "black_food_count": 30 +} \ No newline at end of file diff --git a/levels/010.json b/levels/010.json new file mode 100644 index 0000000..845f344 --- /dev/null +++ b/levels/010.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1200, + "playground_size": [ + 700, + 600 + ], + "score_to_pass": 25, + "green_food_count": 20, + "black_food_count": 40 +} \ No newline at end of file diff --git a/levels/template.json b/levels/template.json new file mode 100644 index 0000000..8e70077 --- /dev/null +++ b/levels/template.json @@ -0,0 +1,10 @@ +{ + "time_to_play": 1200, + "playground_size": [ + 500, + 200 + ], + "score_to_pass": 20, + "green_food_count": 3, + "black_food_count": 1 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6b60966..936367f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -mlgame>=9.5.1.7b0 \ No newline at end of file +mlgame>=10.3.1 \ No newline at end of file diff --git a/src/enums.py b/src/enums.py new file mode 100644 index 0000000..67d67c9 --- /dev/null +++ b/src/enums.py @@ -0,0 +1,6 @@ + +from enum import auto +from mlgame.utils.enum import StringEnum +class FoodTypeEnum(StringEnum): + GREEN = auto() + RED = auto() diff --git a/src/game.py b/src/game.py index f44dbcf..f69c57d 100644 --- a/src/game.py +++ b/src/game.py @@ -1,4 +1,6 @@ -import time +import copy +import json +import os.path from os import path import pygame @@ -6,11 +8,17 @@ import pygame from mlgame.game.paia_game import PaiaGame, GameResultState, GameStatus from mlgame.utils.enum import get_ai_name from mlgame.view.decorator import check_game_progress, check_game_result -from mlgame.view.view_model import create_text_view_data, create_asset_init_data, create_image_view_data, Scene, \ - create_scene_progress_data +from mlgame.view.view_model import * +from .enums import FoodTypeEnum from .game_object import Ball, Food +BG_COLOR = "#111111" +PG_COLOR = "#B3E5FC" + +WIDTH = 800 +HEIGHT = 600 ASSET_PATH = path.join(path.dirname(__file__), "../asset") +LEVEL_PATH = path.join(path.dirname(__file__), "../levels") class EasyGame(PaiaGame): @@ -18,20 +26,74 @@ class EasyGame(PaiaGame): This is a Interface of a game """ - def __init__(self, time_to_play, total_point_count, score, color, *args, **kwargs): + def __init__( + self, time_to_play, score_to_pass, green_food_count, red_food_count, + playground_size: list, + level: int = -1, + # level_file, + *args, **kwargs): super().__init__(user_num=1) + self.game_result_state = GameResultState.FAIL - self.scene = Scene(width=800, height=600, color="#4FC3F7", bias_x=0, bias_y=0) - print(color) - self.ball = Ball("#" + color) + self.scene = Scene(width=WIDTH, height=HEIGHT, color=BG_COLOR, bias_x=0, bias_y=0) + self._level = level + if level != -1: + + self.set_game_params_by_level(level) + pass + else: + self._playground_w = int(playground_size[0]) + self._playground_h = int(playground_size[1]) + self.playground = pygame.Rect( + 0, 0, + self._playground_w, + self._playground_h + ) + self._green_food_count = green_food_count + self._red_food_count = red_food_count + self._score_to_pass = score_to_pass + self._frame_limit = time_to_play + self.playground.center = (WIDTH / 2, HEIGHT / 2) + self.foods = pygame.sprite.Group() + self.init_game() + + def set_game_params_by_level(self, level): + level_file_path =os.path.join(LEVEL_PATH, f"{level:03d}.json") + if os.path.exists(level_file_path): + # If the file exists, load parameters from the file + with open(level_file_path) as f: + game_params = json.load(f) + else: + # If the file doesn't exist, use default parameters + + with open(os.path.join(LEVEL_PATH, "001.json")) as f: + game_params = json.load(f) + self._level=1 + + + self._playground_w = int(game_params["playground_size"][0]) + self._playground_h = int(game_params["playground_size"][1]) + self.playground = pygame.Rect( + 0, 0, + self._playground_w, + self._playground_h + ) + self._green_food_count = int(game_params["green_food_count"]) + self._red_food_count = int(game_params["black_food_count"]) + self._score_to_pass = int(game_params["score_to_pass"]) + self._frame_limit = int(game_params["time_to_play"]) + self.playground.center = (WIDTH / 2, HEIGHT / 2) + + + def init_game(self): + self.ball = Ball() + self.foods.empty() self.score = 0 - self.score_to_win = score - self._create_foods(total_point_count) - self._begin_time = time.time() - self._timer = 0 + self._create_foods(self._green_food_count, FoodTypeEnum.GREEN) + self._create_foods(self._red_food_count, FoodTypeEnum.RED) self.frame_count = 0 - self.time_limit = time_to_play + self._frame_count_down = self._frame_limit def update(self, commands): # handle command @@ -40,24 +102,31 @@ class EasyGame(PaiaGame): action = ai_1p_cmd[0] else: action = "NONE" - # print(ai_1p_cmd) - self.ball.update(action) + self.ball.update(action) + self.revise_ball(self.ball, self.playground) # update sprite self.foods.update() # handle collision - hits = pygame.sprite.spritecollide(self.ball, self.foods, True, pygame.sprite.collide_rect_ratio(0.8)) + hits = pygame.sprite.spritecollide(self.ball, self.foods, True) if hits: - self.score += len(hits) - self._create_foods(len(hits)) - self._timer = round(time.time() - self._begin_time, 3) + for food in hits: + if food.type == FoodTypeEnum.GREEN: + self.score += 1 + self._create_foods(1, FoodTypeEnum.GREEN) + + elif food.type == FoodTypeEnum.RED: + self._create_foods(1, FoodTypeEnum.RED) + self.score -= 1 + # self._timer = round(time.time() - self._begin_time, 3) self.frame_count += 1 + self._frame_count_down = self._frame_limit - self.frame_count # self.draw() if not self.is_running: - return "QUIT" + return "RESET" def get_data_from_game_to_player(self): """ @@ -86,31 +155,38 @@ class EasyGame(PaiaGame): if self.is_running: status = GameStatus.GAME_ALIVE - elif self.score > self.score_to_win: + elif self.score > self._score_to_pass: status = GameStatus.GAME_PASS else: status = GameStatus.GAME_OVER return status def reset(self): + if self.score > self._score_to_pass and self._level != -1: + # win and use level will enter next level + self._level += 1 + self.set_game_params_by_level(self._level) + + self.init_game() + pass @property def is_running(self): - return self.frame_count < self.time_limit + return self.frame_count < self._frame_limit def get_scene_init_data(self): """ Get the initial scene and object information for drawing on the web """ # TODO add music or sound - bg_path = path.join(ASSET_PATH, "img/background.jpg") - background = create_asset_init_data( - "background", 800, 600, bg_path, - github_raw_url="https://raw.githubusercontent.com/PAIA-Playful-AI-Arena/easy_game/main/asset/img/background.jpg") + # bg_path = path.join(ASSET_PATH, "img/background.jpg") + # background = create_asset_init_data( + # "background", WIDTH, HEIGHT, bg_path, + # github_raw_url="https://raw.githubusercontent.com/PAIA-Playful-AI-Arena/easy_game/main/asset/img/background.jpg") scene_init_data = {"scene": self.scene.__dict__, "assets": [ - background + # background ], # "audios": {} } @@ -126,9 +202,21 @@ class EasyGame(PaiaGame): foods_data.append(food.game_object_data) game_obj_list = [self.ball.game_object_data] game_obj_list.extend(foods_data) - backgrounds = [create_image_view_data("background", 0, 0, 800, 600)] - foregrounds = [create_text_view_data(f"Score = {str(self.score)}", 650, 50, "#FF0000", "24px Arial BOLD")] - toggle_objs = [create_text_view_data(f"Timer = {str(self._timer)} s", 650, 100, "#FFAA00", "24px Arial")] + backgrounds = [ + # create_image_view_data("background", 0, 0, WIDTH, HEIGHT), + create_rect_view_data( + "playground", self.playground.x, self.playground.y, + self.playground.w, self.playground.h, PG_COLOR) + ] + foregrounds = [ + + ] + toggle_objs = [ + create_text_view_data(f"Score:{self.score:04d}", 600, 50, "#A5D6A7", "24px Arial BOLD"), + create_text_view_data(f" Next:{self._score_to_pass:04d}", 600, 100, "#FF4081", "24px Arial BOLD"), + create_text_view_data(f" Time:{self._frame_count_down:04d}", 600, 150, "#FF5722", "24px Arial BOLD"), + + ] scene_progress = create_scene_progress_data(frame=self.frame_count, background=backgrounds, object_list=game_obj_list, foreground=foregrounds, toggle=toggle_objs) @@ -147,7 +235,8 @@ class EasyGame(PaiaGame): { "player": get_ai_name(0), "rank": 1, - "score": self.score + "score": self.score, + "passed": self.score > self._score_to_pass } ] @@ -171,8 +260,25 @@ class EasyGame(PaiaGame): cmd_1p.append("NONE") return {get_ai_name(0): cmd_1p} - def _create_foods(self, count: int = 5): + def _create_foods(self, count: int = 5, type: FoodTypeEnum = FoodTypeEnum.GREEN): for i in range(count): # add food to group - food = Food(self.foods) + food = Food(self.foods, type) + + food.rect.centerx = random.randint(self.playground.left, self.playground.right) + food.rect.centery = random.randint(self.playground.top, self.playground.bottom) + pass + + def revise_ball(self, ball: Ball, playground: pygame.Rect): + ball_rect = copy.deepcopy(ball.rect) + if ball_rect.left < playground.left: + ball_rect.left = playground.left + elif ball_rect.right > playground.right: + ball_rect.right = playground.right + + if ball_rect.top < playground.top: + ball_rect.top = playground.top + elif ball_rect.bottom > playground.bottom: + ball_rect.bottom = playground.bottom + ball.rect = ball_rect pass diff --git a/src/game_object.py b/src/game_object.py index 9eafbfb..bda8172 100644 --- a/src/game_object.py +++ b/src/game_object.py @@ -1,25 +1,28 @@ -import random - import pygame.sprite +from games.easy_game.src.enums import FoodTypeEnum from mlgame.view.view_model import create_rect_view_data +FOOD_COLOR_MAP = {FoodTypeEnum.GREEN: "#009688", + FoodTypeEnum.RED: "#FF1744"} +BALL_COLOR = "#FFEB3B" BALL_VEL = 10.5 -BALL_H = 50 +BALL_H = 30 -BALL_W = 50 +BALL_W = 30 class Ball(pygame.sprite.Sprite): - def __init__(self, color="#FFEB3B"): + def __init__(self): pygame.sprite.Sprite.__init__(self) self.origin_image = pygame.Surface([BALL_W, BALL_H]) self.image = self.origin_image - self.color = color + self.color = BALL_COLOR self.rect = self.image.get_rect() self.rect.center = (400, 300) + def update(self, motion): # for motion in motions: if motion == "UP": @@ -51,28 +54,25 @@ class Ball(pygame.sprite.Sprite): class Food(pygame.sprite.Sprite): - def __init__(self, group): + def __init__(self, group, type: FoodTypeEnum): pygame.sprite.Sprite.__init__(self, group) self.image = pygame.Surface([8, 8]) - self.color = "#E91E63" + self.type = type + self.color = FOOD_COLOR_MAP[type] + self.rect = self.image.get_rect() - self.rect.centerx = random.randint(0, 800) - self.rect.centery = random.randint(0, 600) self.angle = 0 def update(self) -> None: - self.angle += 10 - if self.angle > 360: - self.angle -= 360 + pass @property def game_object_data(self): - return {"type": "rect", - "name": "ball", - "x": self.rect.x, - "y": self.rect.y, - "angle": 0, - "width": self.rect.width, - "height": self.rect.height, - "color": self.color - } + return create_rect_view_data( + "food", + self.rect.x, + self.rect.y, + self.rect.width, + self.rect.height, + self.color + )