diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..8c7862f Binary files /dev/null and b/__pycache__/__init__.cpython-39.pyc differ diff --git a/__pycache__/config.cpython-39.pyc b/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000..e3da738 Binary files /dev/null and b/__pycache__/config.cpython-39.pyc differ diff --git a/ai_clients/__init__.py b/ai_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai_clients/__pycache__/client_template.cpython-39.pyc b/ai_clients/__pycache__/client_template.cpython-39.pyc new file mode 100644 index 0000000..d79fb5a Binary files /dev/null and b/ai_clients/__pycache__/client_template.cpython-39.pyc differ diff --git a/ai_clients/client_template.py b/ai_clients/client_template.py new file mode 100644 index 0000000..41fbe77 --- /dev/null +++ b/ai_clients/client_template.py @@ -0,0 +1,24 @@ +import random + + +class MLPlay: + def __init__(self, ai_name:str,*args, **kwargs): + print(f"Initial {__file__} script with ai_name:{ai_name}") + + + def update(self, scene_info: dict, *args, **kwargs): + """ + Generate the command according to the received scene information + """ + # print("AI received data from game :", json.dumps(scene_info)) + # print(scene_info) + actions = ["UP", "DOWN", "LEFT", "RIGHT"] + + return random.sample(actions, 1) + + def reset(self): + """ + Reset the status + """ + print("reset ml script") + pass diff --git a/ai_clients/client_with_error_in_update.py b/ai_clients/client_with_error_in_update.py new file mode 100644 index 0000000..5f9c4ce --- /dev/null +++ b/ai_clients/client_with_error_in_update.py @@ -0,0 +1,26 @@ +import random + + +class MLPlay: + def __init__(self, *args, **kwargs): + print("Initial ml script") + self.count=0 + + def update(self, scene_info: dict, *args, **kwargs): + """ + Generate the command according to the received scene information + """ + # print("AI received data from game :", json.dumps(scene_info)) + # print(scene_info) + actions = ["UP", "DOWN", "LEFT", "RIGHT"] + self.count+=1 + if self.count >100: + a = actions[100] + return random.sample(actions, 1) + + def reset(self): + """ + Reset the status + """ + print("reset ml script") + pass diff --git a/ai_clients/client_with_syntax_error.py b/ai_clients/client_with_syntax_error.py new file mode 100644 index 0000000..507adb1 --- /dev/null +++ b/ai_clients/client_with_syntax_error.py @@ -0,0 +1,22 @@ +import random + +class MLPlay: + def __init__(self, *args, **kwargs) + print("Initial ml script") + + def update(self, scene_info: dict, *args, **kwargs): + """ + Generate the command according to the received scene information + """ + # print("AI received data from game :", json.dumps(scene_info)) + # print(scene_info) + actions = ["UP", "DOWN", "LEFT", "RIGHT"] + + return random.sample(actions, 1) + + def reset(self): + """ + Reset the status + """ + print("reset ml script") + pass diff --git a/ai_clients/client_with_unvalid_import.py b/ai_clients/client_with_unvalid_import.py new file mode 100644 index 0000000..4c16862 --- /dev/null +++ b/ai_clients/client_with_unvalid_import.py @@ -0,0 +1,24 @@ +import random +import aaa + + +class MLPlay: + def __init__(self, *args, **kwargs): + print("Initial ml script") + + def update(self, scene_info: dict, *args, **kwargs): + """ + Generate the command according to the received scene information + """ + # print("AI received data from game :", json.dumps(scene_info)) + # print(scene_info) + actions = ["UP", "DOWN", "LEFT", "RIGHT"] + + return random.sample(actions, 1) + + def reset(self): + """ + Reset the status + """ + print("reset ml script") + pass diff --git a/ai_clients/manual.py b/ai_clients/manual.py new file mode 100644 index 0000000..e85c808 --- /dev/null +++ b/ai_clients/manual.py @@ -0,0 +1,36 @@ +import pygame + + +class MLPlay: + def __init__(self, ai_name: str, *args, **kwargs): + self.ai_name = ai_name + print(f"Initial {__file__} script with ai_name:{ai_name}") + + def update(self, scene_info: dict, keyboard: list = [], *args, **kwargs): + """ + Generate the command according to the received scene information + """ + # print("AI received data from game :", json.dumps(scene_info)) + # print(scene_info) + actions = [] + + if pygame.K_w in keyboard or pygame.K_UP in keyboard: + actions.append("UP") + elif pygame.K_s in keyboard or pygame.K_DOWN in keyboard: + actions.append("DOWN") + + elif pygame.K_a in keyboard or pygame.K_LEFT in keyboard: + actions.append("LEFT") + elif pygame.K_d in keyboard or pygame.K_RIGHT in keyboard: + actions.append("RIGHT") + else: + actions.append("NONE") + + return actions + + def reset(self): + """ + Reset the status + """ + print("reset ml script") + pass diff --git a/asset/easy_game.gif b/asset/easy_game.gif new file mode 100644 index 0000000..6d8cff9 Binary files /dev/null and b/asset/easy_game.gif differ diff --git a/asset/img/background.jpg b/asset/img/background.jpg new file mode 100644 index 0000000..37411a5 Binary files /dev/null and b/asset/img/background.jpg differ diff --git a/blockly.json b/blockly.json new file mode 100644 index 0000000..412873b --- /dev/null +++ b/blockly.json @@ -0,0 +1,34 @@ +{ + "GAME_STATUS": [ + ["GAME_ALIVE", "alive", "存活"], + ["GAME_PASS", "pass", "通關"], + ["GAME_OVER", "over", "失敗"] + ], + "SCENE_INFO": [ + ["scene_info['frame']", "# frame", "# 幀數"], + ["scene_info['status']", "game status", "遊戲狀態"], + ["scene_info['ball_x']", "x coordinate of ball", "球的 x 座標"], + ["scene_info['ball_y']", "y coordinate of ball", "球的 y 座標"], + ["scene_info['score']", "x coordinate of platform", "平台的 x 座標"], + ["scene_info['foods']", "list of foods positions", "點點的位置清單"], + ["scene_info", "dictionary of all information", "包含所有資訊的字典"] + ], + "CONSTANT": [ + [0, "left boundary", "左邊界"], + [800, "right boundary", "右邊界"], + [0, "top boundary", "上邊界"], + [600, "bottom boundary", "下邊界"], + [50, "ball width", "球身的寬度"], + [50, "ball height", "球身的高度"], + [8, "food width", "食物的寬度"], + [8, "food height", "食物的高度"] + ], + "ACTION": [ + ["['UP']", "moving up", "向上移動"], + ["['DOWN']", "moving down", "向下移動"], + ["['LEFT']", "moving left", "向左移動"], + ["['RIGHT']", "moving right", "向右移動"], + ["['NONE']", "doing nothing", "不動作"] + + ] +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..d9f3e6c --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +import sys +from os import path +sys.path.append(path.dirname(__file__)) + +from mlgame.argument.tool import read_json_file, parse_config +from src.game import EasyGame + +config_file = path.join(path.dirname(__file__), "game_config.json") + + +config_data = read_json_file(config_file) +GAME_VERSION = config_data["version"] +GAME_PARAMS = parse_config(config_data) + +# will be equal to config. GAME_SETUP["ml_clients"][0]["name"] + +GAME_SETUP = { + "game": EasyGame, + # "dynamic_ml_clients":True +} diff --git a/game_config.json b/game_config.json new file mode 100644 index 0000000..9b6b9b7 --- /dev/null +++ b/game_config.json @@ -0,0 +1,54 @@ +{ + "game_name": "easy_game", + "version": "2.0.0", + "url": "None", + "game_params": [ + { + "name": "time_to_play", + "verbose": "遊戲總幀數", + "type": "int", + "max": 2000, + "min": 600, + "default": 600, + "help": "set the limit of frame count , actually time will be revised according to your FPS ." + }, + { + "name": "total_point_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", + "verbose": "通關分數", + "type": "int", + "min": 1, + "max": 30, + "default": 10, + "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" + } + ] +} \ No newline at end of file diff --git a/game_development_tutorial.md b/game_development_tutorial.md new file mode 100644 index 0000000..08d4848 --- /dev/null +++ b/game_development_tutorial.md @@ -0,0 +1,192 @@ +# Easy Game +這是一個簡單的遊戲,主要用來示範如何在PAIA 上發布一個遊戲 +![](asset/easy_game.gif) +## Game Config +遊戲參數定義檔案需要使用json格式,且需要命名為`game_config.json` +```json +{ + "game_name": "easy_game", // 遊戲的名稱 + "version": "1.0.1", // 版本號 + "url": "None", // github 專案連結 + "game_params": [ // 遊戲參數陣列 + { + "name": "time_to_play", // 遊戲參數的名字 + "verbose": "遊戲總幀數", // 顯示文字 + "type": "int", // 類型 分為 int str + "max": 2000, // int 可以設定 最大最小值 + "min": 600, + "default": 600, // 遊戲參數的預設值 +// 參數的輔助說明 + "help": "set the limit of frame count , actually time will be revised according to your FPS .", + + }, + { + "name": "color", + "verbose": "矩形顏色", + "type": "str", + "choices": [ +// 字串的選項需要有顯示文字(verbose) 與 實際值(value) + { + "verbose": "CYAN", + "value": "00BCD4" + }, + { + "verbose": "YELLOW", + "value": "FFEB3B" + }, + { + "verbose": "ORANGE", + "value": "FF9800" + } + ], + "help": "set the color of rectangle", + "default": "FFEB3B" + } + ] +} +``` +## Game Blockly +```json +{ + "GAME_STATUS": [ +// 遊戲狀態的參數 +// ['python 變數名稱','display in English','中文的顯示文字'] + ["GAME_ALIVE", "alive", "存活"], + ["GAME_PASS", "pass", "通關"], + ["GAME_OVER", "over", "失敗"] + ], + "SCENE_INFO": [ +// 傳給MLPlay的資料內容 +// ["python code", "english", "chinese"], + ["scene_info['frame']", "# frame", "# 幀數"], + ["scene_info['status']", "game status", "遊戲狀態"], + ["scene_info['ball_x']", "x coordinate of ball", "球的 x 座標"], + ["scene_info['ball_y']", "y coordinate of ball", "球的 y 座標"], + ["scene_info['score']", "x coordinate of platform", "平台的 x 座標"], + ["scene_info['foods']", "list of foods positions", "點點的位置清單"], + ["scene_info", "dictionary of all information", "包含所有資訊的字典"] + ], + "CONSTANT": [ +// 提供給玩家的常數資訊 +// ["value", "english", "chinese"], + [0, "left boundary", "左邊界"], + [800, "right boundary", "右邊界"], + [0, "top boundary", "上邊界"], + [600, "bottom boundary", "下邊界"], + [50, "ball width", "球身的寬度"], + [50, "ball height", "球身的高度"], + [8, "food width", "食物的寬度"], + [8, "food height", "食物的高度"] + ], + "ACTION": [ +// 讓玩家使用的遊戲指令 +// ["python code", "english", "chinese"], + ["UP", "moving up", "向上移動"], + ["DOWN", "moving down", "向下移動"], + ["LEFT", "moving left", "向左移動"], + ["RIGHT", "moving right", "向右移動"], + ["NONE", "doing nothing", "不動作"] + + ] +} +``` +## Game interface +遊戲需要實作一個interface +```python + +class PaiaGame(abc.ABC): + + def __init__(self): + """ + 初始化資料 + """ + pass + + @abc.abstractmethod + def update(self, commands): + self.frame_count += 1 + + @abc.abstractmethod + def game_to_player_data(self) -> dict: + """ + send something to game AI + we could send different data to different ai + """ + to_players_data = {} + + return to_players_data + + @abc.abstractmethod + def reset(self): + pass + + @abc.abstractmethod + def get_scene_init_data(self) -> dict: + """ + Get the initial scene and object information for drawing on the web + """ + # TODO add music or sound + scene_init_data = {"scene": self.scene.__dict__, + "assets": [ + + ], + # "audios": {} + } + return scene_init_data + + @abc.abstractmethod + def get_scene_progress_data(self) -> dict: + """ + Get the position of game objects for drawing on the web + """ + + scene_progress = { + # background view data will be draw first + "background": [], + # game object view data will be draw on screen by order , and it could be shifted by WASD + "object_list": [], + "toggle": [], + "foreground": [], + # other information to display on web + "user_info": [], + # other information to display on web + "game_sys_info": {} + } + return scene_progress + + @abc.abstractmethod + def get_game_result(self) -> dict: + """ + send game result + """ + return {"frame_used": self.frame_count, + "result": { + + }, + + } + + @abc.abstractmethod + def get_keyboard_command(self) -> dict: + """ + Define how your game will run by your keyboard + """ + cmd_1p = [] + + ai_1p = self.ai_clients()[0]["name"] + return {ai_1p: cmd_1p} + + @staticmethod + def ai_clients() -> list: + """ + let MLGame know how to parse your ai, + you can also use this names to get different cmd and send different data to each ai client + """ + return [ + {"name": "1P"} + ] + +``` + +## Flowchart + diff --git a/main.py b/main.py new file mode 100644 index 0000000..1aa72ef --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +import pygame +import sys + +sys.path.append(r"../..") +from mlgame.view.view import PygameView +from mlgame.gamedev.generic import quit_or_esc +from src.game import EasyGame + +FPS = 30 +if __name__ == '__main__': + pygame.init() + game = EasyGame(time_to_play=1000, total_point_count=10, score=5, color="FF9800") + scene_init_info_dict = game.get_scene_init_data() + game_view = PygameView(scene_init_info_dict) + frame_count = 0 + while game.is_running and not quit_or_esc(): + pygame.time.Clock().tick_busy_loop(FPS) + commands = game.get_keyboard_command() + game.update(commands) + game_progress_data = game.get_scene_progress_data() + game_view.draw(game_progress_data) + frame_count += 1 + # print(frame_count) + + pygame.quit() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-39.pyc b/src/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..39ca635 Binary files /dev/null and b/src/__pycache__/__init__.cpython-39.pyc differ diff --git a/src/__pycache__/game.cpython-39.pyc b/src/__pycache__/game.cpython-39.pyc new file mode 100644 index 0000000..218b768 Binary files /dev/null and b/src/__pycache__/game.cpython-39.pyc differ diff --git a/src/__pycache__/game_object.cpython-39.pyc b/src/__pycache__/game_object.cpython-39.pyc new file mode 100644 index 0000000..669b164 Binary files /dev/null and b/src/__pycache__/game_object.cpython-39.pyc differ diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..02ab660 --- /dev/null +++ b/src/game.py @@ -0,0 +1,188 @@ +import time +from os import path + +import pygame + +from mlgame.argument.model import AI_NAMES +from mlgame.gamedev.paia_game import PaiaGame, GameResultState, GameStatus +from mlgame.tests.test_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 .game_object import Ball, Food + +ASSET_PATH = path.join(path.dirname(__file__), "../asset") + + +class EasyGame(PaiaGame): + """ + This is a Interface of a game + """ + + def __init__(self, time_to_play, total_point_count, score, color): + super().__init__() + 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.foods = pygame.sprite.Group() + self.score = 0 + self.score_to_win = score + self._create_foods(total_point_count) + self._begin_time = time.time() + self._timer = 0 + self.frame_count = 0 + self.time_limit = time_to_play + + def update(self, commands): + # handle command + ai_1p_cmd = commands[AI_NAMES[0]] + if ai_1p_cmd is not None: + action = ai_1p_cmd[0] + else: + action = "NONE" + # print(ai_1p_cmd) + self.ball.update(action) + + # update sprite + self.foods.update() + + # handle collision + hits = pygame.sprite.spritecollide(self.ball, self.foods, True, pygame.sprite.collide_rect_ratio(0.8)) + if hits: + self.score += len(hits) + self._create_foods(len(hits)) + self._timer = round(time.time() - self._begin_time, 3) + + self.frame_count += 1 + # self.draw() + + if not self.is_running: + return "QUIT" + + def game_to_player_data(self): + """ + send something to game AI + we could send different data to different ai + """ + to_players_data = {} + foods_data = [] + for food in self.foods: + foods_data.append({"x": food.rect.x, "y": food.rect.y}) + data_to_1p = { + "frame": self.frame_count, + "ball_x": self.ball.rect.centerx, + "ball_y": self.ball.rect.centery, + "foods": foods_data, + "score": self.score, + "status": self.get_game_status() + } + + to_players_data[AI_NAMES[0]] = data_to_1p + # should be equal to config. GAME_SETUP["ml_clients"][0]["name"] + + return to_players_data + + def get_game_status(self): + + if self.is_running: + status = GameStatus.GAME_ALIVE + elif self.score > self.score_to_win: + status = GameStatus.GAME_PASS + else: + status = GameStatus.GAME_OVER + return status + + def reset(self): + pass + + @property + def is_running(self): + return self.frame_count < self.time_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, "url") + scene_init_data = {"scene": self.scene.__dict__, + "assets": [ + background + ], + # "audios": {} + } + return scene_init_data + + @check_game_progress + def get_scene_progress_data(self): + """ + Get the position of game objects for drawing on the web + """ + foods_data = [] + for food in self.foods: + 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")] + scene_progress = create_scene_progress_data(frame=self.frame_count, background=backgrounds, + object_list=game_obj_list, + foreground=foregrounds, toggle=toggle_objs) + return scene_progress + + @check_game_result + def get_game_result(self): + """ + send game result + """ + if self.get_game_status() == GameStatus.GAME_PASS: + self.game_result_state = GameResultState.FINISH + return {"frame_used": self.frame_count, + "state": self.game_result_state, + "attachment": [ + { + "player": AI_NAMES[0], + "rank": 1, + "score": self.score + } + ] + + } + + def get_keyboard_command(self): + """ + Define how your game will run by your keyboard + """ + cmd_1p = [] + key_pressed_list = pygame.key.get_pressed() + if key_pressed_list[pygame.K_UP]: + cmd_1p.append("UP") + elif key_pressed_list[pygame.K_DOWN]: + cmd_1p.append("DOWN") + elif key_pressed_list[pygame.K_LEFT]: + cmd_1p.append("LEFT") + elif key_pressed_list[pygame.K_RIGHT]: + cmd_1p.append("RIGHT") + else: + cmd_1p.append("NONE") + return {AI_NAMES[0]: cmd_1p} + + def _create_foods(self, count: int = 5): + for i in range(count): + # add food to group + food = Food(self.foods) + pass + + @staticmethod + def ai_clients(): + """ + let MLGame know how to parse your ai, + you can also use this names to get different cmd and send different data to each ai client + """ + return [ + {"name": "1P", + + }, + ] diff --git a/src/game_object.py b/src/game_object.py new file mode 100644 index 0000000..2d5c3c9 --- /dev/null +++ b/src/game_object.py @@ -0,0 +1,63 @@ +import random + +import pygame.sprite + + +class Ball(pygame.sprite.Sprite): + def __init__(self, color="#FFEB3B"): + pygame.sprite.Sprite.__init__(self) + self.image = pygame.Surface([50, 50]) + self.color = color + self.rect = self.image.get_rect() + self.rect.center = (400, 300) + + def update(self, motion): + # for motion in motions: + if motion == "UP": + self.rect.centery -= 10.5 + elif motion == "DOWN": + self.rect.centery += 10.5 + elif motion == "LEFT": + self.rect.centerx -= 10.5 + elif motion == "RIGHT": + self.rect.centerx += 10.5 + + @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 + } + + +class Food(pygame.sprite.Sprite): + def __init__(self, group): + pygame.sprite.Sprite.__init__(self, group) + self.image = pygame.Surface([8, 8]) + self.color = "#E91E63" + 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 + + @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 + }