├── games ├── __init__.py ├── snake │ ├── __init__.py │ ├── ml │ │ ├── __init__.py │ │ └── ml_play_template.py │ ├── game │ │ ├── __init__.py │ │ ├── gamecore.py │ │ ├── snake.py │ │ └── gameobject.py │ ├── config.py │ ├── README_zh-TW.md │ └── README.md ├── arkanoid │ ├── __init__.py │ ├── game │ │ ├── __init__.py │ │ ├── level_data │ │ │ ├── 1.dat │ │ │ ├── 2.dat │ │ │ ├── 3.dat │ │ │ ├── 5.dat │ │ │ └── 4.dat │ │ ├── arkanoid.py │ │ ├── gamecore.py │ │ └── gameobject.py │ ├── ml │ │ ├── __init__.py │ │ └── ml_play_template.py │ ├── config.py │ ├── README_zh-TW.md │ └── README.md └── pingpong │ ├── __init__.py │ ├── game │ ├── __init__.py │ ├── gamecore.py │ ├── pingpong.py │ └── gameobject.py │ ├── ml │ ├── __init__.py │ ├── ml_play_template.py │ └── ml_play_manual.py │ ├── config.py │ ├── README_zh-TW.md │ └── README.md ├── mlgame ├── __init__.py ├── crosslang │ ├── __init__.py │ ├── compile │ │ ├── __init__.py │ │ └── cpp │ │ │ ├── __init__.py │ │ │ ├── include │ │ │ ├── base_main.cpp │ │ │ ├── ml_play.hpp │ │ │ └── mlgame_client.hpp │ │ │ └── main.py │ ├── ext_lang_map.py │ ├── exceptions.py │ ├── main.py │ ├── ml_play.py │ ├── client.py │ ├── README.md │ └── DOCUMENTATION.md ├── gamedev │ ├── __init__.py │ ├── generic.py │ └── physics.py ├── utils │ ├── __init__.py │ ├── enum.py │ ├── delegate.py │ └── argparser_generator.py ├── _version.py ├── errno.py ├── exceptions.py ├── recorder.py ├── process.py ├── gameconfig.py ├── execution_command.py ├── execution.py ├── communication.py └── loops.py ├── requirements.txt ├── .gitattributes ├── MLGame.py ├── .gitignore ├── README.md └── CHANGELOG.md /games/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/snake/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/snake/ml/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mlgame/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mlgame/crosslang/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /games/arkanoid/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/pingpong/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/snake/game/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mlgame/gamedev/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mlgame/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/arkanoid/game/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/arkanoid/ml/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/pingpong/game/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /games/pingpong/ml/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mlgame/crosslang/compile/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame==1.9.6 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /mlgame/crosslang/compile/cpp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mlgame/_version.py: -------------------------------------------------------------------------------- 1 | version = "MLGame Beta 8.0.1" 2 | -------------------------------------------------------------------------------- /MLGame.py: -------------------------------------------------------------------------------- 1 | from mlgame.execution import execute 2 | 3 | if __name__ == "__main__": 4 | execute() 5 | -------------------------------------------------------------------------------- /games/arkanoid/game/level_data/1.dat: -------------------------------------------------------------------------------- 1 | 25 50 -1 2 | 10 0 0 3 | 35 0 0 4 | 60 0 0 5 | 85 0 0 6 | 110 0 0 7 | -------------------------------------------------------------------------------- /mlgame/crosslang/ext_lang_map.py: -------------------------------------------------------------------------------- 1 | # The mapping of file extension to the supported language 2 | EXTESION_LANG_MAP = { 3 | ".cpp": "cpp" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyproj 3 | *.pyproj.user 4 | *.sln 5 | *.zip 6 | *.pickle 7 | *.sav 8 | __pycache__/ 9 | venv/* 10 | .vs/* 11 | ml_play*.py 12 | !ml_play_template.py 13 | !ml_play_manual.py 14 | -------------------------------------------------------------------------------- /mlgame/errno.py: -------------------------------------------------------------------------------- 1 | # Uncatched python exception 2 | UNHANDLED_ERROR = 1 3 | # An error occurred while handling the execution command 4 | COMMAND_LINE_ERROR = 2 5 | # An error occurred while executing the game 6 | GAME_EXECUTION_ERROR = 3 7 | # An error occurred while compiling the non-python script 8 | COMPILATION_ERROR = 4 9 | -------------------------------------------------------------------------------- /games/snake/config.py: -------------------------------------------------------------------------------- 1 | GAME_VERSION = "1.1" 2 | GAME_PARAMS = { 3 | "()": { 4 | "prog": "snake", 5 | "description": "A simple snake game", 6 | "game_usage": "%(prog)s" 7 | } 8 | } 9 | 10 | from .game.snake import Snake 11 | 12 | GAME_SETUP = { 13 | "game": Snake, 14 | "ml_clients": [ 15 | { "name": "ml" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /games/arkanoid/game/level_data/2.dat: -------------------------------------------------------------------------------- 1 | 25 50 -1 2 | 10 0 0 3 | 35 0 0 4 | 60 0 0 5 | 85 0 0 6 | 110 0 0 7 | 0 10 0 8 | 25 10 0 9 | 50 10 0 10 | 75 10 0 11 | 100 10 0 12 | 125 10 0 13 | 10 20 0 14 | 35 20 0 15 | 60 20 0 16 | 85 20 0 17 | 110 20 0 18 | 0 30 0 19 | 25 30 0 20 | 50 30 0 21 | 75 30 0 22 | 100 30 0 23 | 125 30 0 24 | 10 40 0 25 | 35 40 0 26 | 60 40 0 27 | 85 40 0 28 | 110 40 0 29 | -------------------------------------------------------------------------------- /games/arkanoid/game/level_data/3.dat: -------------------------------------------------------------------------------- 1 | 25 50 -1 2 | 10 0 1 3 | 35 0 1 4 | 60 0 1 5 | 85 0 1 6 | 110 0 1 7 | 0 10 1 8 | 25 10 0 9 | 50 10 0 10 | 75 10 0 11 | 100 10 0 12 | 125 10 1 13 | 10 20 1 14 | 35 20 0 15 | 60 20 0 16 | 85 20 0 17 | 110 20 1 18 | 0 30 1 19 | 25 30 0 20 | 50 30 0 21 | 75 30 0 22 | 100 30 0 23 | 125 30 1 24 | 10 40 1 25 | 35 40 1 26 | 60 40 1 27 | 85 40 1 28 | 110 40 1 29 | -------------------------------------------------------------------------------- /games/arkanoid/game/level_data/5.dat: -------------------------------------------------------------------------------- 1 | 25 50 -1 2 | 10 0 1 3 | 35 0 1 4 | 60 0 1 5 | 85 0 1 6 | 110 0 1 7 | 0 10 1 8 | 25 10 0 9 | 50 10 0 10 | 75 10 0 11 | 100 10 0 12 | 125 10 1 13 | 10 20 1 14 | 110 20 1 15 | 0 30 1 16 | 25 30 0 17 | 50 30 0 18 | 75 30 0 19 | 100 30 0 20 | 125 30 1 21 | 10 40 1 22 | 110 40 1 23 | 0 120 1 24 | 50 120 1 25 | 100 120 1 26 | 150 120 1 27 | -25 135 1 28 | 25 135 1 29 | 75 135 1 30 | 125 135 1 31 | 0 150 1 32 | 50 150 1 33 | 100 150 1 34 | 150 150 1 35 | 25 165 1 36 | 75 165 1 37 | 125 165 1 38 | -25 185 1 39 | 0 185 1 40 | 125 185 1 41 | 150 185 1 -------------------------------------------------------------------------------- /mlgame/crosslang/compile/cpp/include/base_main.cpp: -------------------------------------------------------------------------------- 1 | // The user script will be included here. 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | AbstractMLPlay *ml = new MLPlay(get_init_args()); 8 | client_ready(); 9 | 10 | json command; 11 | while (1) { 12 | command = ml->update(get_scene_info()); 13 | if (command == "RESET") { 14 | client_reset(); 15 | ml->reset(); 16 | client_ready(); 17 | continue; 18 | } 19 | send_command(command); 20 | } 21 | 22 | delete ml; 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /mlgame/utils/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | class StringEnum(str, Enum): 4 | def _generate_next_value_(name, start, count, last_values): 5 | return name 6 | 7 | def __eq__(self, other): 8 | if isinstance(other, StringEnum): 9 | return self.value == other.value 10 | elif isinstance(other, str): 11 | return self.value == other 12 | 13 | return False 14 | 15 | def __ne__(self, other): 16 | return not self.__eq__(other) 17 | 18 | def __str__(self): 19 | return self.value 20 | 21 | def __hash__(self): 22 | return hash(self.value) 23 | -------------------------------------------------------------------------------- /games/arkanoid/config.py: -------------------------------------------------------------------------------- 1 | GAME_VERSION = "1.1" 2 | GAME_PARAMS = { 3 | "()": { 4 | "prog": "arkanoid", 5 | "game_usage": "%(prog)s " 6 | }, 7 | "difficulty": { 8 | "choices": ("EASY", "NORMAL"), 9 | "metavar": "difficulty", 10 | "help": "Specify the game style. Choices: %(choices)s" 11 | }, 12 | "level": { 13 | "type": int, 14 | "help": "Specify the level map" 15 | } 16 | } 17 | 18 | from .game.arkanoid import Arkanoid 19 | import pygame 20 | 21 | GAME_SETUP = { 22 | "game": Arkanoid, 23 | "ml_clients": [ 24 | { "name": "ml" } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /mlgame/crosslang/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | The exceptions for the crosslang module 3 | """ 4 | 5 | class CompilationError(Exception): 6 | """ 7 | Exception raised when failed to compile the user script 8 | """ 9 | def __init__(self, file, reason): 10 | self.file = file 11 | self.reason = reason 12 | 13 | def __str__(self): 14 | return "Failed to compile '{}':\n{}".format(self.file, self.reason) 15 | 16 | class MLClientExecutionError(Exception): 17 | """ 18 | Exception raised when an error occurred while running non-python ml script 19 | """ 20 | def __init__(self, message): 21 | """ 22 | Constructor 23 | """ 24 | self.message = message 25 | 26 | def __str__(self): 27 | return self.message 28 | -------------------------------------------------------------------------------- /games/pingpong/ml/ml_play_template.py: -------------------------------------------------------------------------------- 1 | """ 2 | The template of the script for the machine learning process in game pingpong 3 | """ 4 | 5 | class MLPlay: 6 | def __init__(self, side): 7 | """ 8 | Constructor 9 | 10 | @param side A string "1P" or "2P" indicates that the `MLPlay` is used by 11 | which side. 12 | """ 13 | self.ball_served = False 14 | self.side = side 15 | 16 | def update(self, scene_info): 17 | """ 18 | Generate the command according to the received scene information 19 | """ 20 | if scene_info["status"] != "GAME_ALIVE": 21 | return "RESET" 22 | 23 | if not self.ball_served: 24 | self.ball_served = True 25 | return "SERVE_TO_LEFT" 26 | else: 27 | return "MOVE_LEFT" 28 | 29 | def reset(self): 30 | """ 31 | Reset the status 32 | """ 33 | self.ball_served = False 34 | -------------------------------------------------------------------------------- /games/snake/ml/ml_play_template.py: -------------------------------------------------------------------------------- 1 | """ 2 | The template of the script for playing the game in the ml mode 3 | """ 4 | 5 | class MLPlay: 6 | def __init__(self): 7 | """ 8 | Constructor 9 | """ 10 | pass 11 | 12 | def update(self, scene_info): 13 | """ 14 | Generate the command according to the received scene information 15 | """ 16 | if scene_info["status"] == "GAME_OVER": 17 | return "RESET" 18 | 19 | snake_head = scene_info["snake_head"] 20 | food = scene_info["food"] 21 | 22 | if snake_head[0] > food[0]: 23 | return "LEFT" 24 | elif snake_head[0] < food[0]: 25 | return "RIGHT" 26 | elif snake_head[1] > food[1]: 27 | return "UP" 28 | elif snake_head[1] < food[1]: 29 | return "DOWN" 30 | 31 | def reset(self): 32 | """ 33 | Reset the status if needed 34 | """ 35 | pass 36 | -------------------------------------------------------------------------------- /games/arkanoid/ml/ml_play_template.py: -------------------------------------------------------------------------------- 1 | """ 2 | The template of the main script of the machine learning process 3 | """ 4 | 5 | class MLPlay: 6 | def __init__(self): 7 | """ 8 | Constructor 9 | """ 10 | self.ball_served = False 11 | 12 | def update(self, scene_info): 13 | """ 14 | Generate the command according to the received `scene_info`. 15 | """ 16 | # Make the caller to invoke `reset()` for the next round. 17 | if (scene_info["status"] == "GAME_OVER" or 18 | scene_info["status"] == "GAME_PASS"): 19 | return "RESET" 20 | 21 | if not self.ball_served: 22 | self.ball_served = True 23 | command = "SERVE_TO_LEFT" 24 | else: 25 | command = "MOVE_LEFT" 26 | 27 | return command 28 | 29 | def reset(self): 30 | """ 31 | Reset the status 32 | """ 33 | self.ball_served = False 34 | -------------------------------------------------------------------------------- /mlgame/crosslang/main.py: -------------------------------------------------------------------------------- 1 | from .ext_lang_map import EXTESION_LANG_MAP 2 | 3 | import importlib 4 | import os.path 5 | 6 | def compile_script(script_full_path): 7 | """ 8 | Compile the script to an executable according to its file extension 9 | 10 | This function will load the corresponding language module according to 11 | `EXTENSION_LANG_MAP` for compiling the script. 12 | 13 | @param script_full_path The full path of the target script 14 | @return A list of command segments for executing the executable 15 | """ 16 | path_no_ext, extension = os.path.splitext(script_full_path) 17 | compilation_module = importlib.import_module( 18 | ".compile.{}.main".format(EXTESION_LANG_MAP[extension]), __package__) 19 | 20 | script_execution_cmd = compilation_module.compile_script(script_full_path) 21 | if not isinstance(script_execution_cmd, list): 22 | raise TypeError("The returned execution command is not a list.") 23 | 24 | return script_execution_cmd 25 | -------------------------------------------------------------------------------- /games/pingpong/config.py: -------------------------------------------------------------------------------- 1 | GAME_VERSION = "1.2" 2 | 3 | from argparse import ArgumentTypeError 4 | 5 | def positive_int(string): 6 | value = int(string) 7 | if value < 1: 8 | raise ArgumentTypeError() 9 | return value 10 | 11 | GAME_PARAMS = { 12 | "()": { 13 | "prog": "pingpong", 14 | "game_usage": "%(prog)s [game_over_score]" 15 | }, 16 | "difficulty": { 17 | "choices": ("EASY", "NORMAL", "HARD"), 18 | "metavar": "difficulty", 19 | "help": "Specify the game style. Choices: %(choices)s" 20 | }, 21 | "game_over_score": { 22 | "type": positive_int, 23 | "nargs": "?", 24 | "default": 3, 25 | "help": ("[Optional] The score that the game will be exited " 26 | "when either side reaches it.[default: %(default)s]") 27 | } 28 | } 29 | 30 | from .game.pingpong import PingPong 31 | import pygame 32 | 33 | GAME_SETUP = { 34 | "game": PingPong, 35 | "ml_clients": [ 36 | { "name": "ml_1P", "args": ("1P",) }, 37 | { "name": "ml_2P", "args": ("2P",) } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /games/pingpong/ml/ml_play_manual.py: -------------------------------------------------------------------------------- 1 | """ 2 | The script that send the instruction according to the keyboard input 3 | """ 4 | 5 | import pygame 6 | 7 | class MLPlay: 8 | def __init__(self, side): 9 | self._pygame_init() 10 | print("Invisible joystick is used for the {} side." 11 | .format(side)) 12 | 13 | def _pygame_init(self): 14 | pygame.display.init() 15 | pygame.display.set_mode((300, 100)) 16 | pygame.display.set_caption("Invisible joystick") 17 | 18 | def update(self, scene_info): 19 | if scene_info["status"] != "GAME_ALIVE": 20 | return "RESET" 21 | 22 | pygame.event.pump() 23 | key_pressed_list = pygame.key.get_pressed() 24 | if key_pressed_list[pygame.K_LEFT]: 25 | cmd = "MOVE_LEFT" 26 | elif key_pressed_list[pygame.K_RIGHT]: 27 | cmd = "MOVE_RIGHT" 28 | elif key_pressed_list[pygame.K_PERIOD]: 29 | cmd = "SERVE_TO_LEFT" 30 | elif key_pressed_list[pygame.K_SLASH]: 31 | cmd = "SERVE_TO_RIGHT" 32 | else: 33 | cmd = "NONE" 34 | 35 | return cmd 36 | 37 | def reset(self): 38 | pass 39 | -------------------------------------------------------------------------------- /mlgame/utils/delegate.py: -------------------------------------------------------------------------------- 1 | class FunctionDelegate: 2 | """ 3 | Simulate the function delegate 4 | 5 | Invoke the registered function by invoking the instance of FunctionDelegate. 6 | For example: 7 | ```python 8 | func_delegate = FunctionDelegate() 9 | func_delegate.set_function(foo) 10 | func_delegate() # Same as foo() 11 | ``` 12 | """ 13 | 14 | def __init__(self): 15 | self._target_function = None 16 | 17 | def set_function(self, func): 18 | """ 19 | Set the target function to the function delegate 20 | 21 | @param func A function or a callable object 22 | """ 23 | if self._target_function is not None: 24 | raise ValueError("The target function has been already set.") 25 | 26 | if not callable(func): 27 | raise ValueError("The specified 'func' is not callable.") 28 | 29 | self._target_function = func 30 | 31 | def __call__(self, *args, **kwargs): 32 | """ 33 | Invoke the registered target function 34 | 35 | This function is invoked by using the instance of FunctionDelegate as a function. 36 | """ 37 | if self._target_function is None: 38 | raise RuntimeError("The target function is not specified.") 39 | 40 | return self._target_function(*args, **kwargs) 41 | -------------------------------------------------------------------------------- /games/arkanoid/game/level_data/4.dat: -------------------------------------------------------------------------------- 1 | 0 0 0 2 | 0 100 0 3 | 175 100 0 4 | 0 110 0 5 | 25 110 0 6 | 150 110 0 7 | 175 110 0 8 | 0 120 0 9 | 25 120 0 10 | 150 120 0 11 | 175 120 0 12 | 0 130 0 13 | 25 130 0 14 | 150 130 0 15 | 175 130 0 16 | 0 140 0 17 | 25 140 0 18 | 150 140 0 19 | 175 140 0 20 | 0 150 0 21 | 25 150 0 22 | 150 150 0 23 | 175 150 0 24 | 0 160 0 25 | 25 160 0 26 | 150 160 0 27 | 175 160 0 28 | 0 170 0 29 | 25 170 0 30 | 150 170 0 31 | 175 170 0 32 | 0 180 0 33 | 25 180 0 34 | 150 180 0 35 | 175 180 0 36 | 0 190 0 37 | 25 190 0 38 | 150 190 0 39 | 175 190 0 40 | 0 200 0 41 | 25 200 0 42 | 150 200 0 43 | 175 200 0 44 | 0 210 0 45 | 25 210 0 46 | 150 210 0 47 | 175 210 0 48 | 0 220 0 49 | 25 220 0 50 | 150 220 0 51 | 175 220 0 52 | 0 230 0 53 | 25 230 0 54 | 150 230 0 55 | 175 230 0 56 | 0 240 0 57 | 25 240 0 58 | 150 240 0 59 | 175 240 0 60 | 0 250 0 61 | 25 250 0 62 | 150 250 0 63 | 175 250 0 64 | 0 260 0 65 | 25 260 0 66 | 150 260 0 67 | 175 260 0 68 | 0 270 0 69 | 25 270 0 70 | 150 270 0 71 | 175 270 0 72 | 0 280 0 73 | 25 280 0 74 | 150 280 0 75 | 175 280 0 76 | 0 290 0 77 | 25 290 0 78 | 150 290 0 79 | 175 290 0 80 | 0 300 0 81 | 25 300 0 82 | 150 300 0 83 | 175 300 0 84 | 0 310 0 85 | 25 310 0 86 | 150 310 0 87 | 175 310 0 88 | 0 320 0 89 | 25 320 0 90 | 150 320 0 91 | 175 320 0 92 | 0 330 0 93 | 25 330 0 94 | 150 330 0 95 | 175 330 0 96 | 0 340 0 97 | 175 340 0 -------------------------------------------------------------------------------- /mlgame/crosslang/compile/cpp/include/ml_play.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * The API for the user to implement the MLPlay class 3 | */ 4 | #ifndef _ML_PLAY_HPP_ 5 | #define _ML_PLAY_HPP_ 6 | 7 | /* 8 | * Additional json library. Download "json.hpp" from 9 | * https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp 10 | * and put it in the same directory of this file. 11 | */ 12 | #include "json.hpp" 13 | using json = nlohmann::json; 14 | 15 | /* 16 | * The abstract class for MLPlay class 17 | */ 18 | class AbstractMLPlay 19 | { 20 | public: 21 | /* 22 | * Generate the command according to the received scene information 23 | * 24 | * @param scene_info The scene information 25 | * @return The game command or the reset command. The game command should have 26 | * two field. One is "frame", the other is "command". The value of "frame" is an 27 | * integer which is the same as the value of `scene_info["frame"]`. The value of 28 | * "command" is an array in which elements are string of commands. If the value of 29 | * "command" is a string "RESET", then this game command is a reset command. 30 | */ 31 | virtual json update(json scene_info) = 0; 32 | 33 | /* 34 | * The reset function would be invoked while received the reset command. 35 | */ 36 | virtual void reset() = 0; 37 | }; 38 | 39 | #endif // _ML_PLAY_HPP_ -------------------------------------------------------------------------------- /mlgame/crosslang/ml_play.py: -------------------------------------------------------------------------------- 1 | """ 2 | The ml client runs in ml process as the bridge of cross language client and game process. 3 | """ 4 | from .client import Client 5 | 6 | from .exceptions import MLClientExecutionError 7 | 8 | class MLPlay: 9 | def __init__(self, script_execution_cmd, init_args, init_kwargs): 10 | self._client = Client(script_execution_cmd) 11 | 12 | # Pass initial arguments 13 | self._client.send_to_client("__init__", { 14 | "args": init_args, 15 | "kwargs": init_kwargs 16 | }) 17 | 18 | self._wait_ready() 19 | 20 | def update(self, scene_info): 21 | self._client.send_to_client("__scene_info__", scene_info) 22 | return self._recv_from_client() 23 | 24 | def reset(self): 25 | self._wait_ready() 26 | 27 | def _wait_ready(self): 28 | """ 29 | Wait for the ready command from the client 30 | """ 31 | command = self._recv_from_client() 32 | while command != "READY": 33 | command = self._client.recv_from_client() 34 | 35 | def _recv_from_client(self): 36 | """ 37 | Receive the command sent from the client 38 | """ 39 | command = self._client.recv_from_client() 40 | 41 | if isinstance(command, MLClientExecutionError): 42 | raise command 43 | 44 | return command 45 | 46 | def stop_client(self): 47 | """ 48 | Stop the client 49 | """ 50 | self._client.terminate() 51 | -------------------------------------------------------------------------------- /mlgame/crosslang/compile/cpp/include/mlgame_client.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * The library for C++ client to communicate with mlgame 3 | */ 4 | #ifndef _MLGAME_CLIENT_ 5 | #define _MLGAME_CLIENT_ 6 | 7 | #include 8 | 9 | /* 10 | * Additional json library. Download "json.hpp" from 11 | * https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp 12 | * and put it in the same directory of this file. 13 | */ 14 | #include "json.hpp" 15 | using json = nlohmann::json; 16 | 17 | using namespace std; 18 | 19 | /* 20 | * Get the initial arguments 21 | */ 22 | json get_init_args() 23 | { 24 | string init_args_str; 25 | getline(cin, init_args_str); 26 | 27 | // Ignore header "__init__" 28 | init_args_str.erase(0, init_args_str.find_first_of('{')); 29 | 30 | return json::parse(init_args_str); 31 | } 32 | 33 | /* 34 | * Inform the game that the client is ready for receiving the data 35 | */ 36 | void client_ready() 37 | { 38 | cout << "__command__ READY" << endl; 39 | } 40 | 41 | /* 42 | * Inform the game that the client is resetting 43 | */ 44 | void client_reset() 45 | { 46 | cout << "__command__ RESET" << endl; 47 | } 48 | 49 | /* 50 | * Receive the scene info sent from the game 51 | */ 52 | json get_scene_info() 53 | { 54 | string scene_info_str; 55 | getline(cin, scene_info_str); 56 | 57 | // Ignore header "__scene_info__" 58 | scene_info_str.erase(0, scene_info_str.find_first_of('{')); 59 | 60 | return json::parse(scene_info_str); 61 | } 62 | 63 | /* 64 | * Send the game command to the game 65 | */ 66 | void send_command(json command) 67 | { 68 | cout << "__command__ " << command << endl; 69 | } 70 | 71 | #endif //_MLGAME_CLIENT_ 72 | -------------------------------------------------------------------------------- /games/snake/README_zh-TW.md: -------------------------------------------------------------------------------- 1 | # 貪食蛇 2 | 3 | ## 概觀 4 | 5 | ![Imgur](https://i.imgur.com/aVDPwWP.gif) 6 | 7 | 控制蛇的移動,讓牠吃到食物。每吃到一個食物,蛇身就會增長一段。盡可能地吃到最多食物。 8 | 9 | ## 執行 10 | 11 | * 手動模式:`python MLGame.py -m snake` 12 | * 控制蛇的方向:方向鍵 13 | * 蛇一個影格移動一步,可以加入 `-f ` 來降低蛇的移動速度 14 | * 機器學習模式:`python MLGame.py -i ml_play_template.py snake` 15 | 16 | ## 詳細遊戲資訊 17 | 18 | ### 座標系統 19 | 20 | 與打磚塊遊戲一樣 21 | 22 | ### 遊戲區域 23 | 24 | 300 \* 300 像素 25 | 26 | ### 遊戲物件 27 | 28 | #### 蛇 29 | 30 | * 蛇由一系列正方形構成。每一個正方形的大小是 10 \* 10 像素 31 | * 蛇頭顏色是綠色的,而蛇身皆為白色 32 | * 蛇頭初始位置在 (40, 40),而蛇身依序在 (40, 30)、(40, 20)、(40, 10) 33 | * 蛇初始移動方向是向下,每一個影格移動 10 個像素 34 | * 當蛇吃到食物時,蛇身會增長一個 35 | 36 | #### 食物 37 | 38 | * 食物是 10 \* 10 像素大小的正方形,但是其樣貌為紅色圓形 39 | * 食物的位置隨機決定,xy 座標範圍皆為 0 ~ 290,以 10 為一單位決定 40 | 41 | ## 撰寫玩遊戲的程式 42 | 43 | 範例程式在 [`ml/ml_play_template.py`](ml/ml_play_template.py)。 44 | 45 | ### 溝通物件 46 | 47 | #### 場景資訊 48 | 49 | 從遊戲端接受的字典物件,也是用來儲存在紀錄檔中的物件。 50 | 51 | ``` 52 | { 53 | 'frame': 12, 54 | 'status': 'GAME_ALIVE', 55 | 'snake_head': (160, 40), 56 | 'snake_body': [(150, 40), (140, 40), (130, 40)], 57 | 'food': (100, 60) 58 | } 59 | ``` 60 | 61 | 以下是物件的鍵值對應: 62 | 63 | * `"frame"`:整數。標記紀錄的是第幾影格的場景資訊。 64 | * `"status"`:字串。目前的遊戲狀態,會是以下值其中之一: 65 | * `"GAME_ALIVE"`:蛇還活著 66 | * `"GAME_OVER"`:蛇撞到牆或是撞到自己 67 | * `"snake_head"`:`(x, y)` tuple。蛇頭的位置。 68 | * `"snake_body"`:為一個列表物件儲存所有蛇身的位置,從蛇頭後一個蛇身依序紀錄到蛇尾,裡面每一個元素都是 `(x, y)` tuple。 69 | * `"food"`:`(x, y)` tuple。食物的位置。 70 | 71 | #### 遊戲指令 72 | 73 | 傳送給遊戲端的字串,控制蛇的移動方向。 74 | 75 | ``` 76 | 'RIGHT' 77 | ``` 78 | 79 | 以下是可用的指令: 80 | 81 | * `"UP"`:蛇頭向上 82 | * `"DOWN"`:蛇頭向下 83 | * `"LEFT"`:蛇頭向左 84 | * `"RIGHT"`:蛇頭向右 85 | * `"NONE"`:保持前進方向 86 | 87 | ### 紀錄檔 88 | 89 | 在紀錄檔中,機器學習端的名字為 `"ml"`。 90 | -------------------------------------------------------------------------------- /mlgame/exceptions.py: -------------------------------------------------------------------------------- 1 | class ProcessError(Exception): 2 | """ 3 | The base class for the exception occurred in the process 4 | """ 5 | 6 | def __init__(self, process_name, message): 7 | """ 8 | Constructor 9 | 10 | @param process_name The name of the process in which the error occurred 11 | @param message The error message 12 | """ 13 | self.process_name = process_name 14 | self.message = message 15 | 16 | class GameProcessError(ProcessError): 17 | """ 18 | Exception raised when an error occurred in the game process 19 | """ 20 | pass 21 | 22 | class MLProcessError(ProcessError): 23 | """ 24 | Exception raised when an error occurred in the ml process 25 | """ 26 | pass 27 | 28 | def trim_callstack(exception_msg: str, target_user_file: str): 29 | """ 30 | Shorten the call stack to the starting point of the user script 31 | """ 32 | exception_msg_list = exception_msg.splitlines(keepends = True) 33 | # Store title 34 | trimmed_msg = exception_msg_list[0] 35 | 36 | # Find the starting point 37 | i = 0 38 | for i in range(2, len(exception_msg_list)): 39 | if target_user_file in exception_msg_list[i]: 40 | break 41 | 42 | return trimmed_msg + "".join(exception_msg_list[i:]) 43 | 44 | class ExecutionCommandError(Exception): 45 | """ 46 | Exception raised when parsed invalid execution command 47 | """ 48 | def __init__(self, message): 49 | """ 50 | Constructor 51 | """ 52 | self.message = message 53 | 54 | def __str__(self): 55 | return self.message 56 | 57 | class GameConfigError(Exception): 58 | """ 59 | Exception raised when the game provides invalid game config 60 | """ 61 | def __init__(self, message): 62 | """ 63 | Constructor 64 | """ 65 | self.message = message 66 | 67 | def __str__(self): 68 | return self.message 69 | -------------------------------------------------------------------------------- /mlgame/utils/argparser_generator.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | 3 | def get_parser_from_dict(parser_config: dict): 4 | """ 5 | Generate `argparse.ArgumentParser` from `parser_config` 6 | 7 | @param parser_config A dictionary carries parameters for creating `ArgumentParser`. 8 | The key "()" specifies parameters for constructor of `ArgumentParser`, 9 | its value is a dictionary of which the key is the name of parameter and 10 | the value is the value to be passed to that parameter. 11 | The remaining keys of `parser_config` specifies arguments to be added to the parser, 12 | which `ArgumentParser.add_argument() is invoked`. The key is the name 13 | of the argument, and the value is similar to the "()" 14 | but for the `add_argument()`. Note that the name of the key is used as the name 15 | of the argument, but if "name_or_flags" is specified in the dictionary of it, 16 | it will be passed to the `add_argument()` instead. The value of "name_or_flags" 17 | must be a tuple. 18 | An example of `parser_config`: 19 | ``` 20 | { 21 | "()": { 22 | "usage": "game " 23 | }, 24 | "difficulty": { 25 | "choices": ["EASY", "NORMAL"], 26 | "metavar": "difficulty", 27 | "help": "Specify the game style. Choices: %(choices)s" 28 | }, 29 | "level": { 30 | "type": int, 31 | "help": "Specify the level map" 32 | }, 33 | } 34 | ``` 35 | """ 36 | if parser_config.get("()"): 37 | parser = ArgumentParser(**parser_config["()"]) 38 | else: 39 | parser = ArgumentParser() 40 | 41 | for arg_name in parser_config.keys(): 42 | if arg_name != "()": 43 | arg_config = parser_config[arg_name].copy() 44 | 45 | name_or_flag = arg_config.pop("name_or_flags", None) 46 | if not name_or_flag: 47 | name_or_flag = (arg_name, ) 48 | 49 | parser.add_argument(*name_or_flag, **arg_config) 50 | 51 | return parser 52 | -------------------------------------------------------------------------------- /mlgame/crosslang/compile/cpp/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | The cross language handler for C++ 3 | """ 4 | import os 5 | import os.path 6 | import string 7 | import random 8 | 9 | from subprocess import PIPE, Popen 10 | 11 | from mlgame.crosslang.exceptions import CompilationError 12 | 13 | def compile_script(script_full_path): 14 | """ 15 | Compile the script to an executable 16 | 17 | The exception will be raised when failed to compile the script. 18 | 19 | @param script_full_path The full path of the target script 20 | @return The execution command of the executable 21 | """ 22 | lib_dir = os.path.join(os.path.dirname(__file__), "include") 23 | 24 | dir_path = os.path.dirname(script_full_path) 25 | main_script_file_path = _preprocess_script(script_full_path, dir_path) 26 | execute_file_path = os.path.join(dir_path, "ml_play.out") 27 | if os.path.exists(execute_file_path): 28 | os.remove(execute_file_path) 29 | 30 | compile_cmd = [ 31 | "g++", main_script_file_path, "-I" + lib_dir, "--std=c++11", 32 | "-o", execute_file_path 33 | ] 34 | 35 | with Popen(compile_cmd, bufsize = 1, 36 | stdout = PIPE, stderr = PIPE, universal_newlines = True) as p: 37 | outs, errs = p.communicate() 38 | 39 | # Remove the generated script after compilation 40 | os.remove(main_script_file_path) 41 | 42 | if p.returncode != 0: 43 | raise CompilationError(os.path.basename(script_full_path), errs) 44 | 45 | return [execute_file_path] 46 | 47 | def _preprocess_script(user_script_path, outfile_dir): 48 | """ 49 | Append the content of user script to "cpp_include/main.cpp" and save to a new file 50 | 51 | @param user_script_path The path of the user script as one of the source 52 | @param outfile_dir The path of the directory to put the new file 53 | @return The path of the new file 54 | """ 55 | basefile_path = os.path.join(os.path.dirname(__file__), "include", "base_main.cpp") 56 | char_choice = string.ascii_lowercase + string.digits 57 | outfile_name = ("main_" + 58 | "".join([random.choice(char_choice) for _ in range(8)]) + 59 | ".cpp") 60 | outfile_path = os.path.join(outfile_dir, outfile_name) 61 | 62 | with open(outfile_path, "w") as out_file, \ 63 | open(basefile_path, "r") as in_file: 64 | # Include the user file 65 | out_file.write("#include \"{}\"\n".format(os.path.basename(user_script_path))) 66 | for line in in_file: 67 | out_file.write(line) 68 | 69 | return outfile_path 70 | -------------------------------------------------------------------------------- /games/snake/README.md: -------------------------------------------------------------------------------- 1 | # Snake 2 | 3 | ## Overview 4 | 5 | ![Imgur](https://i.imgur.com/aVDPwWP.gif) 6 | 7 | Control the snake, eat as much food as possible. 8 | 9 | ## Execution 10 | 11 | * Manual mode: `python MLGame.py -m snake` 12 | * Controlling: arrow keys 13 | * Perhaps 30 fps is too fast to play. 14 | * ML mode: `python MLGame.py -i ml_play_template.py snake` 15 | 16 | ## Detailed Game Information 17 | 18 | ### Game Coordinate 19 | 20 | The same as the game "Arkanoid" 21 | 22 | ### Game area 23 | 24 | 300 \* 300 pixels 25 | 26 | ### Game objects 27 | 28 | #### Snake 29 | 30 | * The snake is composed of squares. The size of a square is 10 \* 10 pixels. 31 | * The head is a green square, the bodies are white squares. 32 | * The head is initially at (40, 40), and the bodies are at (40, 30), (40, 20), (40, 10). 33 | * The snake initially goes downward. It moves 10 pixels long per frame. 34 | * The snake body will increase one when it eats the food. 35 | 36 | #### Food 37 | 38 | * The food is 10-by-10-pixel square, but its appearance is a red circle. 39 | * The position of the food is randomly decided, which is (0 <= 10 \* m < 300, 0 <= 10 \* n < 300), where m and n are non-negative integers. 40 | 41 | ## Communicate with Game 42 | 43 | View the example of the ml script in [`ml/ml_play_template.py`](ml/ml_play_template.py). 44 | 45 | ### Communication Objects 46 | 47 | #### Scene Information 48 | 49 | A dictionary object sent from the game process, and also an object to be pickled in the record file. 50 | 51 | ``` 52 | { 53 | 'frame': 12, 54 | 'status': 'GAME_ALIVE', 55 | 'snake_head': (160, 40), 56 | 'snake_body': [(150, 40), (140, 40), (130, 40)], 57 | 'food': (100, 60) 58 | } 59 | ``` 60 | 61 | The keys and values of the scene information: 62 | 63 | * `"frame"`: An integer. The number of the frame that this scene information is for. 64 | * `"status"`: A string. The game status at this frame. It's one of the following value: 65 | * `"GAME_ALIVE"`: The snake is still going. 66 | * `"GAME_OVER"`: The snake hits the wall or itself. 67 | * `"snake_head"`: An `(x, y)` tuple. The position of the snake head. 68 | * `"snake_body"`: A list storing the position of snake bodies, and the storing order is from the head to the tail. Each element in the list is an `(x, y)` tuple. 69 | * `"food"`: An `(x, y)` tuple. The position of the food. 70 | 71 | #### Game Command 72 | 73 | A string command sent to the game process for controlling the moving direction of the snake. 74 | 75 | ``` 76 | 'RIGHT' 77 | ``` 78 | 79 | Here are the available commands: 80 | 81 | * `"UP"`: Make the snake move upward. 82 | * `"DOWN"`: Make the snake move downward. 83 | * `"LEFT"`: Make the snake move to left. 84 | * `"RIGHT"`: Make the snake move to right. 85 | * `"NONE"`: Do not change the moving direction of snake. 86 | 87 | ### Log File 88 | 89 | The name of the ml client in the log flie is `"ml"`. 90 | -------------------------------------------------------------------------------- /mlgame/gamedev/generic.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import time 3 | 4 | def quit_or_esc() -> bool: 5 | """ 6 | Check if the quit event is triggered or the ESC key is pressed. 7 | """ 8 | for event in pygame.event.get(): 9 | if (event.type == pygame.QUIT or 10 | (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE)): 11 | return True 12 | return False 13 | 14 | class KeyCommandMap: 15 | """ 16 | Map the keys to the commands and return the mapped command when the corresponding 17 | key is pressed 18 | """ 19 | def __init__(self, command_map: dict): 20 | """ 21 | Constructor 22 | 23 | @param command_map A dict which maps the keys to the commands. 24 | The key of the dict is the key-code defined in pygame, and 25 | the value is the command that will be returned when the corresponding 26 | key is pressed. 27 | """ 28 | if not isinstance(command_map, dict): 29 | raise TypeError("The 'command_map' should be a 'dict'.") 30 | 31 | self._command_map = command_map 32 | 33 | def get_pressed_commands(self): 34 | """ 35 | Check the pressed keys and return the corresponding commands 36 | 37 | @return A list of commands of which corresponding keys are pressed 38 | If there is no registered key pressed, return an empty list. 39 | """ 40 | key_pressed_list = pygame.key.get_pressed() 41 | pressed_commands = [] 42 | for key, command in self._command_map.items(): 43 | if key_pressed_list[key]: 44 | pressed_commands.append(command) 45 | 46 | return pressed_commands 47 | 48 | class FPSCounter: 49 | """ 50 | The counter for calculating the FPS 51 | 52 | Invoke `get_FPS()` at each frame. The counter will count how many calls within 53 | a specified updating interval. Within a updating interval, the returned FPS value 54 | won't be updated until the starting of next updating interval. 55 | """ 56 | 57 | def __init__(self, update_interval = 1.0): 58 | """ 59 | Constructor 60 | 61 | @param update_interval The time interval in seconds for updating the FPS value 62 | """ 63 | self._update_interval = update_interval 64 | self._fps = 0 65 | self._tick_count = 0 66 | self._last_time_updated = time.time() 67 | 68 | def get_FPS(self) -> int: 69 | """ 70 | Update and get the calculated FPS 71 | """ 72 | self._tick_count += 1 73 | 74 | current_time = time.time() 75 | if current_time - self._last_time_updated > self._update_interval: 76 | self._fps = int(round(self._tick_count / (current_time - self._last_time_updated))) 77 | self._tick_count = 0 78 | self._last_time_updated = current_time 79 | 80 | return self._fps -------------------------------------------------------------------------------- /games/arkanoid/README_zh-TW.md: -------------------------------------------------------------------------------- 1 | # Arkanoid 打磚塊 2 | 3 | ## 概觀 4 | 5 | 6 | 7 | 回合一開始,可以決定發球的位置與方向。發球後嘗試接到回彈的球,並打掉所有磚塊。 8 | 9 | 打磚塊有兩個難度:簡單與普通,在普通難度中會加入切球的機制,可以在板子接球的時候,藉由移動板子來改變球的速度或方向。在一些關卡內有紅色的硬磚塊,需要打兩次才能被破壞,但是透過切球來加速球的移動速度,則可以打一次就破壞該磚塊。 10 | 11 | ## 執行 12 | 13 | * 手動模式:`python MLGame.py -m arkanoid ` 14 | * 將球發到左邊/右邊:`A` 或 `D` 15 | * 移動板子:左右方向鍵 16 | * 機器學習模式:`python MLGame.py -i ml_play_template.py arkanoid ` 17 | 18 | ### 遊戲參數 19 | 20 | * `difficulty`:遊戲難度 21 | * `EASY`:簡單的打磚塊遊戲 22 | * `NORMAL`:加入切球機制 23 | * `level_id`:指定關卡地圖。可以指定的關卡地圖皆在 `game/level_data/` 裡 24 | 25 | ## 詳細遊戲資訊 26 | 27 | ### 座標系 28 | 29 | 使用 pygame 的座標系統,原點在遊戲區域的左上角,x 正方向為向右,y 正方向為向下。遊戲物件的座標皆在物件的左上角,並非中心點。 30 | 31 | ### 遊戲區域 32 | 33 | 200 \* 500 像素 34 | 35 | ### 遊戲物件 36 | 37 | #### 球 38 | 39 | * 5 \* 5 像素的藍色方形 40 | * 每一影格的移動速度是 (±7, ±7) 41 | * 球會從板子所在的位置發出,可以選擇往左或往右發球。如果在 150 影格內沒有發球,則會自動往隨機兩個方向發球 42 | 43 | #### 板子 44 | 45 | * 40 \* 5 像素的綠色長方形 46 | * 每一影格的移動速度是 (±5, 0) 47 | * 初始位置在 (75, 400) 48 | 49 | #### 切球機制 50 | 51 | 球的 x 方向速度會因為接球時板子的移動方向而改變: 52 | 53 | * 如果板子與球的移動方向相同,則球的 x 方向速度會增為 ±10,可以一次打掉硬磚塊 54 | * 如果板子不動,則球的 x 方向速度會回復為 ±7 55 | * 如果板子與球的移動方向相反,則球會被打回原來來的方向,速度會回復為 ±7 56 | 57 | 此機制加入在普通難度中。 58 | 59 | #### 磚塊 60 | 61 | * 25 \* 10 的橘色長方形 62 | * 其位置由關卡地圖決定 63 | 64 | #### 硬磚塊 65 | 66 | * 與磚塊類似,但是紅色的 67 | * 硬磚塊要被打兩次才會被破壞。其被球打一次後,會變為一般磚塊。但是如果被加速後的球打到,則可以直接被破壞 68 | 69 | ## 撰寫玩遊戲的程式 70 | 71 | 範例程式碼在 [`ml_play_template.py`](./ml/ml_play_template.py)。 72 | 73 | ### 溝通物件 74 | 75 | #### 場景資訊 76 | 77 | 從遊戲端接受到的字典物件。 78 | 79 | ``` 80 | { 81 | 'frame': 10, 82 | 'status': 'GAME_ALIVE', 83 | 'ball': (30, 332), 84 | 'platform': (30, 400), 85 | 'bricks': [(35, 50), (60, 50), (85, 50), (110, 50), (135, 50)], 86 | 'hard_bricks': [] 87 | } 88 | ``` 89 | 90 | 該字典物件的鍵值對應: 91 | 92 | * `"frame"`:整數。紀錄的是第幾影格的場景資訊。 93 | * `"status"`:字串。目前的遊戲狀態,會是以下值的其中之一: 94 | * `"GAME_ALIVE"`:遊戲進行中 95 | * `"GAME_PASS"`:所有磚塊都被破壞 96 | * `"GAME_OVER"`:平台無法接到球 97 | * `"ball"`:`(x, y)` tuple。球的位置。 98 | * `"platform"`:`(x, y)` tuple。平台的位置。 99 | * `"bricks"`:為一個 list,裡面每個元素皆為 `(x, y)` tuple。剩餘的普通磚塊的位置,包含被打過一次的硬磚塊。 100 | * `"hard_bricks"`:為一個 list,裡面每個元素皆為 `(x, y)` tuple。剩餘的硬磚塊位置。 101 | 102 | #### 遊戲指令 103 | 104 | 傳給遊戲端,用來控制平台的移動的字串。 105 | 106 | ``` 107 | 'MOVE_LEFT' 108 | ``` 109 | 110 | 可用的指令有: 111 | 112 | * `"SERVE_TO_LEFT"`:將球發往左邊 113 | * `"SERVE_TO_RIGHT"`:將球發往右邊 114 | * `"MOVE_LEFT"`:將平台往左移動 115 | * `"MOVE_RIGHT"`:將平台往右移動 116 | * `"NONE"`:平台無動作 117 | 118 | ### 紀錄檔 119 | 120 | 在紀錄檔中,機器學習端的名字為 `"ml"`。 121 | 122 | ## 自訂關卡地圖 123 | 124 | 你可以將自訂的關卡地圖放在 `game/level_data/` 裡,並給其一個獨特的 `.dat` 作為檔名。 125 | 126 | 在地圖檔中,每一行由三個數字構成,分別代表磚塊的 x 和 y 座標,與磚塊類型。檔案的第一行是標記所有方塊的座標補正 (offset),因此方塊的最終座標為指定的座標加上第一行的座標補正。而磚塊類型的值,0 代表一般磚塊,1 代表硬磚塊,而第一行的磚塊類型值永遠是 -1,例如: 127 | ``` 128 | 25 50 -1 129 | 10 0 0 130 | 35 10 0 131 | 60 20 1 132 | ``` 133 | 代表這個地圖檔有三個磚塊,其座標分別為 (35, 50)、(60, 60),、(85, 70),而第三個磚塊是硬磚塊。 134 | 135 | ## 關於球的物理 136 | 137 | 如果球撞進其他遊戲物件或是遊戲邊界,球會被直接「擠出」到碰撞面上,而不是補償碰撞距離給球。 138 | 139 | ![Imgur](https://i.imgur.com/ouk3Jzh.png) 140 | -------------------------------------------------------------------------------- /mlgame/crosslang/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | The subprocess for running the client script and handling the I/O of it 3 | """ 4 | import json 5 | 6 | from subprocess import PIPE, Popen 7 | from threading import Thread, Event 8 | from queue import Queue 9 | 10 | from .exceptions import MLClientExecutionError 11 | 12 | class Client(Popen): 13 | """ 14 | The subprocess for executing and communication with non-python client 15 | """ 16 | 17 | def __init__(self, execution_cmd: list): 18 | """ 19 | Constructor 20 | """ 21 | super().__init__(execution_cmd, bufsize = 1, 22 | stdin = PIPE, stdout = PIPE, stderr = PIPE, 23 | universal_newlines = True) 24 | 25 | # Thread for reading message 26 | self._command_obj_queue = Queue() 27 | self._is_program_exited = Event() 28 | self._read_stdout_thread = Thread(target = self._read_stdout, 29 | name = "read_stdout") 30 | 31 | self._read_stdout_thread.start() 32 | 33 | def send_to_client(self, header, dict_payload): 34 | """ 35 | Send the dictionary object to the client 36 | The object will be converted into a string: 37 | "
". 38 | 39 | @param header The header to be added at the begin of the message 40 | @param dict_payload A dictionay object 41 | """ 42 | if not self._is_program_exited.is_set(): 43 | self.stdin.write(header + " " + json.dumps(dict_payload) + "\n") 44 | self.stdin.flush() 45 | 46 | def recv_from_client(self): 47 | """ 48 | Receive the command from the subprocess 49 | It will wait until there has command arrived. 50 | """ 51 | return self._command_obj_queue.get() 52 | 53 | def _read_stdout(self): 54 | """ 55 | Read the message from stdout of the client. 56 | If the message contains "__command__" header, it will be pushed to 57 | the command object queue. Otherwise, it will be printed out. 58 | """ 59 | while True: 60 | message = self.stdout.readline() 61 | if "__command__" in message: 62 | # Remove header and the trailing newline 63 | message = message[12:-1] 64 | if message == "READY" or message == "RESET": 65 | self._command_obj_queue.put(message) 66 | else: 67 | self._command_obj_queue.put(json.loads(message)) 68 | elif len(message): 69 | print(message, flush = True) 70 | else: # zero-length read, program is exited 71 | break 72 | 73 | self._is_program_exited.set() 74 | self.wait() 75 | 76 | if self.returncode != 0: 77 | message = " Get output from stderr:\n" + self._read_stderr() 78 | else: 79 | message = "" 80 | 81 | self._command_obj_queue.put(MLClientExecutionError( 82 | "The user program is exited with returncode {}.{}" 83 | .format(self.returncode, message))) 84 | 85 | def _read_stderr(self): 86 | """ 87 | Read the message from stderr of the client. 88 | """ 89 | message = self.stderr.readlines() 90 | return "".join(message) 91 | -------------------------------------------------------------------------------- /games/snake/game/gamecore.py: -------------------------------------------------------------------------------- 1 | """ 2 | The file for managing gameobjects in the scene 3 | """ 4 | 5 | import random 6 | 7 | from pygame import Rect 8 | from pygame.sprite import Group 9 | 10 | from mlgame.utils.enum import StringEnum, auto 11 | 12 | from .gameobject import Snake, Food 13 | 14 | class GameStatus(StringEnum): 15 | GAME_OVER = auto() 16 | GAME_ALIVE = auto() 17 | 18 | class Scene: 19 | """ 20 | The main game scene 21 | """ 22 | 23 | area_rect = Rect(0, 0, 300, 300) 24 | 25 | def __init__(self): 26 | self._create_scene() 27 | 28 | self.score = 0 29 | self._frame = 0 30 | self._status = GameStatus.GAME_ALIVE 31 | 32 | def _create_scene(self): 33 | """ 34 | Import gameobjects to the scene and add them to the draw group 35 | """ 36 | self._snake = Snake() 37 | self._food = Food() 38 | self._random_food_pos() 39 | 40 | self._draw_group = Group() 41 | self._draw_group.add(self._snake.head, *self._snake.body, self._food) 42 | 43 | def _random_food_pos(self): 44 | """ 45 | Randomly set the position of the food 46 | """ 47 | while True: 48 | candidate_pos = ( 49 | random.randrange(0, Scene.area_rect.width, 10), 50 | random.randrange(0, Scene.area_rect.height, 10)) 51 | 52 | if (candidate_pos != self._snake.head_pos and 53 | not self._snake.is_body_pos(candidate_pos)): 54 | break 55 | 56 | self._food.pos = candidate_pos 57 | 58 | def reset(self): 59 | self.score = 0 60 | self._frame = 0 61 | self._status = GameStatus.GAME_ALIVE 62 | 63 | self._snake = Snake() 64 | self._random_food_pos() 65 | self._draw_group.empty() 66 | self._draw_group.add(self._snake.head, *self._snake.body, self._food) 67 | 68 | def draw_gameobjects(self, surface): 69 | """ 70 | Draw gameobjects to the given surface 71 | """ 72 | self._draw_group.draw(surface) 73 | 74 | def update(self, action): 75 | """ 76 | Update the scene 77 | 78 | @param action The action for controlling the movement of the snake 79 | """ 80 | self._frame += 1 81 | self._snake.move(action) 82 | 83 | if self._snake.head_pos == self._food.pos: 84 | self.score += 1 85 | self._random_food_pos() 86 | new_body = self._snake.grow() 87 | self._draw_group.add(new_body) 88 | 89 | if (not Scene.area_rect.collidepoint(self._snake.head_pos) or 90 | self._snake.is_body_pos(self._snake.head_pos)): 91 | self._status = GameStatus.GAME_OVER 92 | 93 | return self._status 94 | 95 | def get_scene_info(self): 96 | """ 97 | Get the current scene information 98 | """ 99 | scene_info = { 100 | "frame": self._frame, 101 | "status": self._status.value, 102 | "snake_head": self._snake.head_pos, 103 | "snake_body": [body.pos for body in self._snake.body], 104 | "food": self._food.pos 105 | } 106 | 107 | return scene_info 108 | -------------------------------------------------------------------------------- /games/pingpong/README_zh-TW.md: -------------------------------------------------------------------------------- 1 | # 乒乓球 2 | 3 | **遊戲版本:1.1** 4 | 5 | ## 概觀 6 | 7 | 8 | 9 | 在回合開始時,可以決定發球位置與方向。如果沒有在 150 影格內發球,球會從平台目前位置隨機往左或往右發球。球速從 7 開始,每 100 影格增加 1。如果球速超過 40 卻還沒分出勝負的話,該回合為平手。 10 | 11 | 在不同的難度中加入兩種機制。一個是切球,球的 x 方向速度會因為板子接球時的移動而改變;另一個是在場地中央會有一個移動的障礙物。 12 | 13 | ## 執行 14 | 15 | * 手動模式:`python MLGame.py -m pingpong [game_over_score]` 16 | * 將球發往左邊/右邊:1P - `.`、`/`,2P - `Q`、`E` 17 | * 移動板子:1P - 左右方向鍵,2P - `A`、`D` 18 | * 機器學習模式:`python MLGame.py -i ml_play_template.py pingpong [game_over_score]` 19 | 20 | ### 遊戲參數 21 | 22 | * `difficulty`:遊戲難度 23 | * `EASY`:簡單的乒乓球遊戲 24 | * `NORMAL`:加入切球機制 25 | * `HARD`:加入切球機制與障礙物 26 | * `game_over_score`:[選填] 指定遊戲結束的分數。當任一方得到指定的分數時,就結束遊戲。預設是 3,但如果啟動遊戲時有指定 `-1` 選項,則結束分數會是 1。 27 | 28 | ## 詳細遊戲資料 29 | 30 | ### 座標系 31 | 32 | 與打磚塊遊戲一樣 33 | 34 | ### 遊戲區域 35 | 36 | 500 \* 200 像素。1P 在下半部,2P 在上半部 37 | 38 | ### 遊戲物件 39 | 40 | #### 球 41 | 42 | * 5 \* 5 像素大小的綠色正方形 43 | * 每場遊戲開始時,都是由 1P 先發球,之後每個回合輪流發球 44 | * 球由板子的位置發出,可以選擇往左或往右發球。如果沒有在 150 影格內發球,則會自動往隨機一個方向發球 45 | * 初始球速是每個影格 (±7, ±7),發球後每 100 影格增加 1 46 | 47 | #### 板子 48 | 49 | * 40 \* 30 的矩形,1P 是紅色的,2P 是藍色的 50 | * 板子移動速度是每個影格 (±5, 0) 51 | * 1P 板子的初始位置在 (80, 420),2P 則在 (80, 50) 52 | 53 | #### 切球機制 54 | 55 | 在板子接球時,球的 x 方向速度會因為板子的移動而改變: 56 | 57 | * 如果板子與球往同一個方向移動時,球的 x 方向速度會增加 3 (只增加一次) 58 | * 如果板子沒有移動,則求的 x 方向速度會恢復為目前的基礎速度 59 | * 如果板子與球往相反方向移動時,球會被打回原來過來的方向,其 x 方向速度恢復為目前的基礎速度 60 | 61 | 切球機制加入在 `NORMAL` 與 `HARD` 難度中。 62 | 63 | #### 障礙物 64 | 65 | * 30 \* 20 像素的矩形 66 | * x 初始位置在 0 到 180 之間,每 20 為一單位隨機決定,y 初始位置固定在 240,移動速度為每影格 (±5, 0) 67 | * 障礙物會往復移動,初始移動方向是隨機決定的 68 | * 障礙物不會切球,球撞到障礙物會保持球的速度 69 | 70 | 障礙物加入在 `HARD` 難度中。 71 | 72 | ## 撰寫玩遊戲的程式 73 | 74 | 程式範例在 [`ml/ml_play_template.py`](ml/ml_play_template.py)。 75 | 76 | ### 初始化參數 77 | 78 | * `side`: 字串。其值只會是 `"1P"` 或 `"2P"`,代表這個程式被哪一邊使用。 79 | 80 | ### 溝通物件 81 | 82 | #### 遊戲場景資訊 83 | 84 | 由遊戲端發送的字典物件,同時也是存到紀錄檔的物件。 85 | 86 | ``` 87 | { 88 | 'frame': 42, 89 | 'status': 'GAME_ALIVE', 90 | 'ball': (189, 128), 91 | 'ball_speed': (7, -7), 92 | 'platform_1P': (0, 420), 93 | 'platform_2P': (0, 50), 94 | 'blocker': (50, 240) 95 | } 96 | ``` 97 | 98 | 以下是該字典物件的鍵值對應: 99 | 100 | * `"frame"`:整數。紀錄的是第幾影格的場景資訊 101 | * `"status"`:字串。目前的遊戲狀態,會是以下的值其中之一: 102 | * `"GAME_ALIVE"`:遊戲正在進行中 103 | * `"GAME_1P_WIN"`:這回合 1P 獲勝 104 | * `"GAME_2P_WIN"`:這回合 2P 獲勝 105 | * `"GAME_DRAW"`:這回合平手 106 | * `"ball"`:`(x, y)` tuple。球的位置。 107 | * `"ball_speed"`:`(x, y)` tuple。目前的球速。 108 | * `"platform_1P"`:`(x, y)` tuple。1P 板子的位置。 109 | * `"platform_2P"`:`(x, y)` tuple。2P 板子的位置。 110 | * `"blocker"`:`(x, y)` tuple。障礙物的位置。如果選擇的難度不是 `HARD`,則其值為 `None`。 111 | 112 | #### 遊戲指令 113 | 114 | 傳給遊戲端的字串,用來控制板子的指令。 115 | 116 | ``` 117 | 'MOVE_RIGHT' 118 | ``` 119 | 120 | 以下是可用的指令: 121 | 122 | * `"SERVE_TO_LEFT"`:將球發向左邊 123 | * `"SERVE_TO_RIGHT"`:將球發向右邊 124 | * `"MOVE_LEFT"`:將板子往左移 125 | * `"MOVE_RIGHT"`:將板子往右移 126 | * `"NONE"`:無動作 127 | 128 | ### 紀錄檔 129 | 130 | 在紀錄檔中,機器學習端的名字 1P 為 `"ml_1P"`、2P 為 `"ml_2P"`。 131 | 132 | ## 機器學習模式的玩家程式 133 | 134 | 乒乓球是雙人遊戲,所以在啟動機器學習模式時,可以利用 `-i -i ` 指定兩個不同的玩家程式。如果只有指定一個玩家程式,則兩邊都會使用同一個程式。 135 | 136 | 而在遊戲中有提供 `ml_play_manual.py` 這個程式,它會建立一個手把,讓玩家可以在機器學習模式中手動與另一個程式對玩。使用流程: 137 | 138 | 1. 使用 `python MLGame.py -i ml_play_template.py -i ml_play_manual.py pingpong ` 啟動遊戲。會看到有兩個視窗,其中一個就是手把。終端機會輸出 "Invisible joystick is used. Press Enter to start the 2P ml process." 的訊息。 139 | 140 | 141 | 142 | 2. 將遊戲手把的視窗拉到一旁,並且讓它是目標視窗 (也就是說視窗的標題不是灰色的)。 143 | 144 | 145 | 146 | 3. 按 Enter 鍵讓手把也發出準備指令以開始遊戲,使用左右方向鍵來控制板子移動。 147 | 148 | ## 關於球的物理 149 | 150 | 與打磚塊遊戲的機制相同 151 | -------------------------------------------------------------------------------- /mlgame/recorder.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import time 3 | 4 | from pathlib import Path 5 | from .execution_command import GameMode 6 | 7 | RECORD_FORMAT_VERSION = 2 8 | 9 | def get_recorder(execution_cmd, ml_names): 10 | """ 11 | The helper function for generating a recorder object 12 | """ 13 | if not execution_cmd.record_progress: 14 | return DummyRecorder() 15 | 16 | root_dir_path = Path(__file__).parent.parent 17 | log_dir_path = root_dir_path.joinpath( 18 | "games", execution_cmd.game_name, "log") 19 | 20 | game_params_str = [str(p) for p in execution_cmd.game_params] 21 | filename_prefix = ( 22 | "manual" if execution_cmd.game_mode == GameMode.MANUAL else "ml") 23 | if game_params_str: 24 | filename_prefix += "_" + "_".join(game_params_str) 25 | 26 | return Recorder(ml_names, log_dir_path, filename_prefix) 27 | 28 | class Recorder: 29 | """ 30 | Record the scene information and the game command to the file 31 | """ 32 | def __init__( 33 | self, ml_names: list, saving_directory: Path, 34 | filename_prefix: str = ""): 35 | """ 36 | Constructor 37 | 38 | @param ml_names A list containing the name of all ml clients 39 | @param saving_directory Specify the directory for saving files 40 | @param filename_prefix Specify the prefix of the filename to be generated. 41 | The filename will be "_YYYY-MM-DD_hh-mm-ss.pickle". 42 | """ 43 | self._saving_directory = saving_directory 44 | if not self._saving_directory.exists(): 45 | self._saving_directory.mkdir() 46 | 47 | if not isinstance(filename_prefix, str): 48 | raise TypeError("'filename_prefix' should be the type of 'str'") 49 | self._filename_prefix = filename_prefix 50 | 51 | # Create storing slots for each ml client 52 | game_progress = { 53 | "record_format_version": RECORD_FORMAT_VERSION 54 | } 55 | for name in ml_names: 56 | game_progress[name] = { 57 | "scene_info": [], 58 | "command": [] 59 | } 60 | self._game_progress = game_progress 61 | self._ml_names = ml_names 62 | 63 | def record(self, scene_info_dict: dict, cmd_dict: dict): 64 | """ 65 | Record the scene information and the command 66 | 67 | The received scene information will be stored in a list. 68 | 69 | @param scene_info_dict A dict storing the scene information for each client 70 | @param cmd_dict A dict storing the command received from each client 71 | """ 72 | for name in self._ml_names: 73 | target_slot = self._game_progress[name] 74 | scene_info = scene_info_dict.get(name, None) 75 | if scene_info: 76 | target_slot["scene_info"].append(scene_info) 77 | target_slot["command"].append(cmd_dict.get(name, None)) 78 | 79 | def flush_to_file(self): 80 | """ 81 | Flush the stored objects to the file 82 | """ 83 | filename = time.strftime("%Y-%m-%d_%H-%M-%S") + ".pickle" 84 | 85 | if self._filename_prefix: 86 | filename = self._filename_prefix + "_" + filename 87 | 88 | filepath = self._saving_directory.joinpath(filename) 89 | with open(filepath, "wb") as f: 90 | pickle.dump(self._game_progress, f) 91 | 92 | for name in self._ml_names: 93 | target_slot = self._game_progress[name] 94 | target_slot["scene_info"].clear() 95 | target_slot["command"].clear() 96 | 97 | class DummyRecorder: 98 | """ 99 | The recorder that only proivdes the API of `Recorder` but do nothing 100 | """ 101 | def __init__(self): 102 | pass 103 | 104 | def record(self, scene_info, commands): 105 | pass 106 | 107 | def flush_to_file(self): 108 | pass 109 | -------------------------------------------------------------------------------- /games/snake/game/snake.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from .gamecore import Scene, GameStatus 4 | from .gameobject import SnakeAction 5 | 6 | class Snake: 7 | """ 8 | The game execution manager 9 | """ 10 | def __init__(self): 11 | self._scene = Scene() 12 | self._pygame_init() 13 | 14 | def _pygame_init(self): 15 | """ 16 | Initialize the required pygame module 17 | """ 18 | pygame.display.init() 19 | pygame.display.set_caption("Snake") 20 | self._screen = pygame.display.set_mode( 21 | (Scene.area_rect.width, Scene.area_rect.height + 25)) 22 | 23 | pygame.font.init() 24 | self._font = pygame.font.Font(None, 22) 25 | self._font_pos = (1, Scene.area_rect.width + 5) 26 | 27 | def update(self, command): 28 | """ 29 | Update the game 30 | """ 31 | # Convert the command 32 | command = (SnakeAction(command["ml"]) 33 | if command["ml"] in SnakeAction.__members__ else SnakeAction.NONE) 34 | 35 | # Pass the command to the scene and get the status 36 | game_status = self._scene.update(command) 37 | 38 | # If the game is over, send the reset signal 39 | if game_status == GameStatus.GAME_OVER: 40 | print("Score: {}".format(self._scene.score)) 41 | return "RESET" 42 | 43 | self._draw_screen() 44 | 45 | def _draw_screen(self): 46 | """ 47 | Draw the scene to the display 48 | """ 49 | self._screen.fill((50, 50, 50)) 50 | self._screen.fill((0, 0, 0), Scene.area_rect) 51 | self._scene.draw_gameobjects(self._screen) 52 | 53 | # Draw score 54 | font_surface = self._font.render( 55 | "Score: {}".format(self._scene.score), True, (255, 255, 255)) 56 | self._screen.blit(font_surface, self._font_pos) 57 | 58 | pygame.display.flip() 59 | 60 | def reset(self): 61 | """ 62 | Reset the game 63 | 64 | This function is invoked when the executor receives the reset signal 65 | """ 66 | self._scene.reset() 67 | 68 | def get_player_scene_info(self): 69 | """ 70 | Get the scene information to be sent to the player 71 | """ 72 | return {"ml": self._scene.get_scene_info()} 73 | 74 | def get_keyboard_command(self): 75 | """ 76 | Get the command according to the pressed key 77 | """ 78 | key_pressed_list = pygame.key.get_pressed() 79 | 80 | if key_pressed_list[pygame.K_UP]: command = "UP" 81 | elif key_pressed_list[pygame.K_DOWN]: command = "DOWN" 82 | elif key_pressed_list[pygame.K_LEFT]: command = "LEFT" 83 | elif key_pressed_list[pygame.K_RIGHT]: command = "RIGHT" 84 | else: command = "NONE" 85 | 86 | return {"ml": command} 87 | 88 | def get_game_info(self): 89 | return { 90 | "scene": { 91 | "size": [300, 300] 92 | }, 93 | "game_object": [ 94 | { "name": "snake_head", "size": [10, 10], "color": [31, 204, 42] }, 95 | { "name": "snake_body", "size": [10, 10], "color": [255, 255, 255] }, 96 | { "name": "food", "size": [10, 10], "color": [232, 54, 42] }, 97 | ] 98 | } 99 | 100 | def get_game_progress(self): 101 | scene_info = self._scene.get_scene_info() 102 | 103 | return { 104 | "game_object": { 105 | "snake_head": [scene_info["snake_head"]], 106 | "snake_body": scene_info["snake_body"], 107 | "food": [scene_info["food"]] 108 | } 109 | } 110 | 111 | def get_game_result(self): 112 | scene_info = self._scene.get_scene_info() 113 | 114 | return { 115 | "frame_used": scene_info["frame"], 116 | "result": ["GAME_OVER"], 117 | "score": self._scene.score 118 | } 119 | -------------------------------------------------------------------------------- /games/arkanoid/game/arkanoid.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from .gamecore import GameStatus, PlatformAction, Scene 4 | 5 | class Arkanoid: 6 | def __init__(self, difficulty, level: int): 7 | self._scene = Scene(difficulty, level) 8 | self._pygame_init() 9 | 10 | def _pygame_init(self): 11 | """ 12 | Initial the pygame for drawing 13 | """ 14 | pygame.display.init() 15 | pygame.display.set_caption("Arkanoid") 16 | self._surface = pygame.display.set_mode(Scene.area_rect.size) 17 | 18 | pygame.font.init() 19 | self._font = pygame.font.Font(None, 22) 20 | self._font_pos = (1, self._surface.get_height() - 21) 21 | 22 | def update(self, command): 23 | """ 24 | Update the game 25 | """ 26 | command = (PlatformAction(command["ml"]) 27 | if command["ml"] in PlatformAction.__members__ else PlatformAction.NONE) 28 | 29 | game_status = self._scene.update(command) 30 | self._draw_screen() 31 | 32 | if (game_status == GameStatus.GAME_OVER or 33 | game_status == GameStatus.GAME_PASS): 34 | print(game_status.value) 35 | return "RESET" 36 | 37 | def _draw_screen(self): 38 | """ 39 | Draw the scene to the display 40 | """ 41 | self._surface.fill((0, 0, 0)) 42 | self._scene.draw_gameobjects(self._surface) 43 | 44 | font_surface = self._font.render( 45 | "Catching ball: {}".format(self._scene.catch_ball_times), 46 | True, (255, 255, 255)) 47 | self._surface.blit(font_surface, self._font_pos) 48 | 49 | pygame.display.flip() 50 | 51 | def reset(self): 52 | """ 53 | Reset the game 54 | """ 55 | self._scene.reset() 56 | 57 | def get_player_scene_info(self): 58 | """ 59 | Get the scene information to be sent to the player 60 | """ 61 | return {"ml": self._scene.get_scene_info()} 62 | 63 | def get_keyboard_command(self): 64 | """ 65 | Get the command according to the pressed key command 66 | """ 67 | key_pressed_list = pygame.key.get_pressed() 68 | 69 | if key_pressed_list[pygame.K_a]: command = "SERVE_TO_LEFT" 70 | elif key_pressed_list[pygame.K_d]: command = "SERVE_TO_RIGHT" 71 | elif key_pressed_list[pygame.K_LEFT]: command = "MOVE_LEFT" 72 | elif key_pressed_list[pygame.K_RIGHT]: command = "MOVE_RIGHT" 73 | else: command = "NONE" 74 | 75 | return {"ml": command} 76 | 77 | def get_game_info(self): 78 | """ 79 | Get the scene and object information for drawing on the web 80 | """ 81 | return { 82 | "scene": { 83 | "size": [200, 500] 84 | }, 85 | "game_object": [ 86 | { "name": "ball", "size": [5, 5], "color": [44, 185, 214] }, 87 | { "name": "platform", "size": [40, 5], "color": [66, 226, 126] }, 88 | { "name": "brick", "size": [25, 10], "color": [244, 158, 66] }, 89 | { "name": "hard_brick", "size": [25, 10], "color": [209, 31, 31] }, 90 | ] 91 | } 92 | 93 | def get_game_progress(self): 94 | """ 95 | Get the position of game objects for drawing on the web 96 | """ 97 | scene_info = self._scene.get_scene_info() 98 | 99 | return { 100 | "game_object": { 101 | "ball": [scene_info["ball"]], 102 | "platform": [scene_info["platform"]], 103 | "brick": scene_info["bricks"], 104 | "hard_brick": scene_info["hard_bricks"], 105 | } 106 | } 107 | 108 | def get_game_result(self): 109 | """ 110 | Get the game result for the web 111 | """ 112 | scene_info = self._scene.get_scene_info() 113 | 114 | return { 115 | "frame_used": scene_info["frame"], 116 | "result": [scene_info["status"]], 117 | "brick_remain": len(scene_info["bricks"]), 118 | } 119 | -------------------------------------------------------------------------------- /games/snake/game/gameobject.py: -------------------------------------------------------------------------------- 1 | """ 2 | The file for defining objects in the game 3 | """ 4 | 5 | from pygame import Rect, Surface, draw 6 | from pygame.sprite import Sprite 7 | from collections import deque 8 | 9 | from mlgame.utils.enum import StringEnum, auto 10 | 11 | class Food(Sprite): 12 | def __init__(self): 13 | super().__init__() 14 | 15 | self.rect = Rect(0, 0, 10, 10) 16 | 17 | surface = Surface(self.rect.size) 18 | draw.circle(surface, (232, 54, 42), self.rect.center, 5) 19 | 20 | self.image = surface 21 | 22 | @property 23 | def pos(self): 24 | return self.rect.topleft 25 | 26 | @pos.setter 27 | def pos(self, value): 28 | self.rect.topleft = value 29 | 30 | class SnakeBody(Sprite): 31 | def __init__(self, init_pos, color): 32 | super().__init__() 33 | 34 | self.rect = Rect(init_pos[0], init_pos[1], 10, 10) 35 | 36 | width = self.rect.width 37 | height = self.rect.height 38 | 39 | self.image = Surface((width, height)) 40 | self.image.fill(color) 41 | draw.line(self.image, (0, 0, 0), (width - 1, 0), (width - 1, height - 1)) 42 | draw.line(self.image, (0, 0, 0), (0, height - 1), (width - 1, height - 1)) 43 | 44 | @property 45 | def pos(self): 46 | return self.rect.topleft 47 | 48 | @pos.setter 49 | def pos(self, value): 50 | self.rect.topleft = value 51 | 52 | class SnakeAction(StringEnum): 53 | UP = auto() 54 | DOWN = auto() 55 | LEFT = auto() 56 | RIGHT = auto() 57 | NONE = auto() 58 | 59 | class Snake: 60 | def __init__(self): 61 | self.head = SnakeBody((40, 40), (31, 204, 42)) # Green 62 | 63 | self.body = deque() 64 | self.body_color = (255, 255, 255) # White 65 | # Note the ordering of appending elements 66 | self.body.append(SnakeBody((40, 30), self.body_color)) 67 | self.body.append(SnakeBody((40, 20), self.body_color)) 68 | self.body.append(SnakeBody((40, 10), self.body_color)) 69 | 70 | # Initialize the action to going down 71 | self._action = SnakeAction.DOWN 72 | 73 | @property 74 | def head_pos(self): 75 | return self.head.pos 76 | 77 | def is_body_pos(self, position): 78 | """ 79 | Check if there has a snake body at the given position 80 | """ 81 | for body in self.body: 82 | if body.pos == position: 83 | return True 84 | 85 | return False 86 | 87 | def grow(self): 88 | """ 89 | Add a new snake body at the tail 90 | """ 91 | new_body = SnakeBody(self.body[-1].pos, self.body_color) 92 | self.body.append(new_body) 93 | 94 | return new_body 95 | 96 | def move(self, action): 97 | """ 98 | Move the snake according to the given action 99 | """ 100 | # If there is no action, take the same action as the last frame. 101 | if action == SnakeAction.NONE: 102 | action = self._action 103 | 104 | # If the head will go back to the body, 105 | # take the same action as the last frame. 106 | possible_head_pos = self._get_possible_head_pos(action) 107 | if possible_head_pos == self.body[0].pos: 108 | action = self._action 109 | 110 | # Move the body 1 step ahead 111 | tail = self.body.pop() 112 | tail.pos = self.head.pos 113 | self.body.appendleft(tail) 114 | 115 | # Get the next head position according to the valid action 116 | next_head_pos = self._get_possible_head_pos(action) 117 | self.head.pos = next_head_pos 118 | 119 | # Store the action 120 | self._action = action 121 | 122 | def _get_possible_head_pos(self, action): 123 | """ 124 | Get the possible head position according to the given action 125 | """ 126 | if action == SnakeAction.UP: 127 | move_delta = (0, -10) 128 | elif action == SnakeAction.DOWN: 129 | move_delta = (0, 10) 130 | elif action == SnakeAction.LEFT: 131 | move_delta = (-10, 0) 132 | elif action == SnakeAction.RIGHT: 133 | move_delta = (10, 0) 134 | 135 | return self.head.rect.move(move_delta).topleft 136 | -------------------------------------------------------------------------------- /mlgame/crosslang/README.md: -------------------------------------------------------------------------------- 1 | # `mlgame.crosslang` package 2 | 3 | The package supporting the execution of non-python ml client. 4 | 5 | The scene information sent from the game is converted to json string by [`json.dumps()`](https://docs.python.org/3/library/json.html#json.dumps) and the game command sent back to the game is converted to python object from json string by [`json.loads()`](https://docs.python.org/3/library/json.html#json.loads). The client API will convert json string to an usable object for the script to use, and vice versa. 6 | 7 | For the documenation and how to support another language, please visit [DOCUMENTATION.md](./DOCUMENTATION.md) 8 | 9 | ## Supported Language 10 | 11 | This section list supported languages and examples. Additional libraries may be needed to run the script. Follow instructions below to set up libraries of the target language. 12 | 13 | ### C++ 14 | 15 | The code is compiled with flag `--std=c++11`. A `ml_play.out` file is generated in the same directory of the code after compilation for execution. 16 | 17 | #### Additional Libraries 18 | 19 | 1. Download the `json.hpp` file from https://github.com/nlohmann/json/releases (in the "Assets" section of the release version choiced). 20 | 2. Put `json.hpp` in `mlgame/crosslang/compile/cpp/include/` directory. 21 | 22 | **`json` how to** 23 | 24 | The json string received from the game will be converted to a `json` object, or a `json` object will be converted to a json string. 25 | 26 | Here is an example scene information in json string: 27 | 28 | ```json 29 | { 30 | "status": "GAME_ALIVE", 31 | "ball": [[1, 2], [3, 4]] 32 | } 33 | ``` 34 | 35 | The corresponding `json` object will be: 36 | 37 | ```c++ 38 | json update(json scene_info) 39 | { 40 | cout << scene_info["status"] << endl; // "GAME_ALIVE" 41 | cout << scene_info["ball"][0][0] << " " 42 | << scene_info["ball"][0][1] << endl; // 1 2 43 | } 44 | ``` 45 | 46 | The json string can be generated from a `json` object: 47 | 48 | ```c++ 49 | json j; 50 | 51 | j["direction"] = "LEFT"; 52 | j["action"] = "SPEED_UP"; 53 | j["data"] = {1, 2, 3}; // An array is stored as std::vector 54 | ``` 55 | 56 | The corresponding json string will be: 57 | 58 | ```json 59 | { 60 | "direction": "LEFT", 61 | "action": "SPEED_UP", 62 | "data": [1, 2, 3] 63 | } 64 | ``` 65 | 66 | If the game command is just a string, add it to `json` object directly: 67 | 68 | ```c++ 69 | json j = "TURN_LEFT"; 70 | ``` 71 | 72 | For more examples view: https://github.com/nlohmann/json#examples 73 | 74 | #### class `MLPlay` 75 | 76 | The user script must provide class `MLPlay`. `MLPlay` should be derived from `AbstractMLPlay` which is defined in [`ml_play.hpp`](./compile/cpp/include/ml_play.hpp). Here are member functions: 77 | - `MLPlay(json init_args)`: Initialize the `MLPlay`. 78 | - `init_args`: The initial arguments sent from the game. Access the positional arguments by `init_args["args"][index]` or the keyword arguments by `init_args["kwargs"][name]`. 79 | - `json update(json scene_info) override`: Received the scene information sent from the game and generate the game command. Both data are stored in a `json` object. 80 | - `void reset() override`: Reset the `MLPlay`. This function is called when `update()` returns `"RESET"` if the game is over. 81 | 82 | #### Example 83 | 84 | Here is an example for play game `arkanoid`: 85 | 86 | ```c++ 87 | #include 88 | #include 89 | 90 | using namespace std; 91 | 92 | class MLPlay : public AbstractMLPlay 93 | { 94 | public: 95 | bool is_ball_served = false; 96 | 97 | // The constructor must have 1 parameter "init_args" 98 | // for passing initial arguments 99 | MLPlay(json init_args) 100 | { 101 | cout << init_args << endl; 102 | } 103 | 104 | json update(json scene_info) override 105 | { 106 | json command; 107 | 108 | if (scene_info["status"] == "GAME_OVER" || 109 | scene_info["status"] == "GAME_PASS") { 110 | command = "RESET"; // "RESET" command 111 | return command; 112 | } 113 | 114 | if (!this->is_ball_served) { 115 | command = "SERVE_TO_LEFT"; 116 | this->is_ball_served = true; 117 | } else 118 | command = "MOVE_LEFT"; 119 | 120 | return command; 121 | } 122 | 123 | void reset() override 124 | { 125 | this->is_ball_served = false; 126 | } 127 | }; 128 | ``` 129 | -------------------------------------------------------------------------------- /mlgame/process.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import traceback 3 | 4 | from multiprocessing import Process, Pipe 5 | from .loops import GameMLModeExecutorProperty, MLExecutorProperty 6 | from .exceptions import ProcessError 7 | 8 | class ProcessManager: 9 | """ 10 | Create and manage processes for executing the game and the ml clients 11 | 12 | @var _game_proc_helper The helper object for the game process 13 | @var _ml_proc_helpers A list storing helper objects for all ml processes 14 | @var _ml_proces A list storing process objects running ml processes 15 | """ 16 | 17 | def __init__( 18 | self, game_executor_propty: GameMLModeExecutorProperty, 19 | ml_executor_propties: list): 20 | """ 21 | Constructor 22 | 23 | @param game_executor_propty The property for the game executor 24 | @param ml_executor_proties A list of `MLExecutorProperty` for the ml executors 25 | """ 26 | self._game_executor_propty = game_executor_propty 27 | self._ml_executor_propties = ml_executor_propties 28 | self._ml_procs = [] 29 | 30 | def start(self): 31 | """ 32 | Start the processes 33 | 34 | The ml processes are spawned and started first, and then the main process executes 35 | the game process. After returning from the game process, the ml processes will be 36 | terminated. 37 | 38 | Note that there must be 1 game process and at least 1 ml process set 39 | before calling this function. Otherwise, the RuntimeError will be raised. 40 | """ 41 | if self._game_executor_propty is None: 42 | raise RuntimeError("The game process is not set. Cannot start the ProcessManager") 43 | if len(self._ml_executor_propties) == 0: 44 | raise RuntimeError("No ml process added. Cannot start the ProcessManager") 45 | 46 | self._create_pipes() 47 | self._start_ml_processes() 48 | 49 | returncode = 0 50 | try: 51 | self._start_game_process() 52 | except ProcessError as e: 53 | print("Error: Exception occurred in '{}' process:".format(e.process_name)) 54 | print(e.message) 55 | returncode = -1 56 | 57 | self._terminate() 58 | 59 | return returncode 60 | 61 | def _create_pipes(self): 62 | """ 63 | Create communication pipes for processes 64 | """ 65 | # Create pipes for Game process <-> ml process 66 | for ml_executor_propty in self._ml_executor_propties: 67 | recv_pipe_for_game, send_pipe_for_ml = Pipe(False) 68 | recv_pipe_for_ml, send_pipe_for_game = Pipe(False) 69 | 70 | self._game_executor_propty.comm_manager.add_comm_to_ml( 71 | ml_executor_propty.name, 72 | recv_pipe_for_game, send_pipe_for_game) 73 | ml_executor_propty.comm_manager.set_comm_to_game( 74 | recv_pipe_for_ml, send_pipe_for_ml) 75 | 76 | def _start_ml_processes(self): 77 | """ 78 | Spawn and start all ml processes 79 | """ 80 | for propty in self._ml_executor_propties: 81 | process = Process(target = _ml_process_entry_point, 82 | name = propty.name, args = (propty,)) 83 | process.start() 84 | 85 | self._ml_procs.append(process) 86 | 87 | def _start_game_process(self): 88 | """ 89 | Start the game process 90 | """ 91 | _game_process_entry_point(self._game_executor_propty) 92 | 93 | def _terminate(self): 94 | """ 95 | Stop all spawned ml processes if it exists 96 | """ 97 | for ml_process in self._ml_procs: 98 | # Send stop signal to all alive ml processes 99 | if ml_process.is_alive(): 100 | self._game_executor_propty.comm_manager.send_to_ml( 101 | None, ml_process.name) 102 | 103 | def _game_process_entry_point(propty: GameMLModeExecutorProperty): 104 | """ 105 | The real entry point of the game process 106 | """ 107 | from .loops import GameMLModeExecutor 108 | 109 | executor = GameMLModeExecutor(propty) 110 | executor.start() 111 | 112 | def _ml_process_entry_point(propty: MLExecutorProperty): 113 | """ 114 | The real entry point of the ml process 115 | """ 116 | from .loops import MLExecutor 117 | 118 | executor = MLExecutor(propty) 119 | executor.start() 120 | -------------------------------------------------------------------------------- /mlgame/gameconfig.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle the game defined config 3 | """ 4 | 5 | import importlib 6 | import inspect 7 | 8 | from .exceptions import GameConfigError 9 | 10 | CONFIG_FILE_NAME = "config.py" 11 | 12 | class GameConfig: 13 | """ 14 | The data class storing the game defined config 15 | """ 16 | 17 | def __init__(self, game_name): 18 | """ 19 | Parse the game defined config and generate a `GameConfig` instance 20 | """ 21 | game_config = self._load_game_config(game_name) 22 | 23 | self.game_version = getattr(game_config, "GAME_VERSION", "") 24 | self.game_params = getattr(game_config, "GAME_PARAMS", { 25 | "()": { 26 | "prog": game_name, 27 | "game_usage": "%(prog)s" 28 | } 29 | }) 30 | self._process_game_param_dict() 31 | 32 | try: 33 | self.game_setup = getattr(game_config, "GAME_SETUP") 34 | except AttributeError: 35 | raise GameConfigError("Missing 'GAME_SETUP' in the game config") 36 | 37 | self._process_game_setup_dict() 38 | 39 | def _load_game_config(self, game_name): 40 | """ 41 | Load the game config 42 | """ 43 | try: 44 | game_config = importlib.import_module(f"games.{game_name}.config") 45 | except ModuleNotFoundError as e: 46 | failed_module_name = e.__str__().split("'")[1] 47 | if failed_module_name == "games." + game_name: 48 | msg = ( 49 | f"Game '{game_name}' dosen't exist or " 50 | "it doesn't provide '__init__.py' in the game directory") 51 | else: 52 | msg = f"Game '{game_name}' dosen't provide '{CONFIG_FILE_NAME}'" 53 | raise GameConfigError(msg) 54 | else: 55 | return game_config 56 | 57 | def _process_game_param_dict(self): 58 | """ 59 | Convert some fields in `GAME_PARAMS` 60 | """ 61 | param_dict = self.game_params 62 | 63 | # Append the prefix of MLGame.py usage to the `game_usage` 64 | # and set it to the `usage` 65 | if param_dict.get("()") and param_dict["()"].get("game_usage"): 66 | game_usage = str(param_dict["()"].pop("game_usage")) 67 | param_dict["()"]["usage"] = "python MLGame.py [options] " + game_usage 68 | 69 | # If the game not specify "--version" flag, 70 | # try to convert `GAME_VERSION` to a flag 71 | if not param_dict.get("--version"): 72 | param_dict["--version"] = { 73 | "action": "version", 74 | "version": self.game_version 75 | } 76 | 77 | def _process_game_setup_dict(self): 78 | """ 79 | Process the value of `GAME_SETUP` 80 | 81 | The `GAME_SETUP` is a dictionary which has several keys: 82 | - "game": Specify the class of the game to be execute 83 | - "dynamic_ml_clients": (Optional) Whether the number of ml clients is decided by 84 | the number of input scripts. 85 | - "ml_clients": A list containing the information of the ml client. 86 | Each element in the list is a dictionary in which members are: 87 | - "name": A string which is the name of the ml client. 88 | - "args": (Optional) A tuple which contains the initial positional arguments 89 | to be passed to the ml client. 90 | - "kwargs": (Optional) A dictionary which contains the initial keyword arguments 91 | to be passed to the ml client. 92 | """ 93 | try: 94 | game_cls = self.game_setup["game"] 95 | ml_clients = self.game_setup["ml_clients"] 96 | except KeyError as e: 97 | raise GameConfigError( 98 | f"Missing {e} in 'GAME_SETUP' in '{CONFIG_FILE_NAME}'") 99 | 100 | # Check if the specified name is existing or duplicated 101 | ml_names = [] 102 | for client in ml_clients: 103 | client_name = client.get("name", "") 104 | if not client_name: 105 | raise GameConfigError( 106 | "'name' in 'ml_clients' of 'GAME_SETUP' " 107 | f"in '{CONFIG_FILE_NAME}' is empty or not existing") 108 | if client_name in ml_names: 109 | raise GameConfigError( 110 | f"Duplicated name '{client_name}' in 'ml_clients' of 'GAME_SETUP' " 111 | f"in '{CONFIG_FILE_NAME}'") 112 | ml_names.append(client_name) 113 | 114 | if not self.game_setup.get("dynamic_ml_clients"): 115 | self.game_setup["dynamic_ml_clients"] = False 116 | 117 | if self.game_setup["dynamic_ml_clients"] and len(ml_clients) == 1: 118 | print( 119 | f"Warning: 'dynamic_ml_clients' in 'GAME_SETUP' in '{CONFIG_FILE_NAME}' " 120 | "is invalid for just one ml client. Set to False.") 121 | self.game_setup["dynamic_ml_clients"] = False 122 | -------------------------------------------------------------------------------- /games/pingpong/game/gamecore.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import random 3 | from mlgame.utils.enum import StringEnum, auto 4 | 5 | from .gameobject import ( 6 | Ball, Blocker, Platform, PlatformAction, SERVE_BALL_ACTIONS 7 | ) 8 | 9 | color_1P = (219, 70, 92) # Red 10 | color_2P = (84, 149, 255) # Blue 11 | 12 | class Difficulty(StringEnum): 13 | EASY = auto() 14 | NORMAL = auto() 15 | HARD = auto() 16 | 17 | class GameStatus(StringEnum): 18 | GAME_1P_WIN = auto() 19 | GAME_2P_WIN = auto() 20 | GAME_DRAW = auto() 21 | GAME_ALIVE = auto() 22 | 23 | class Scene: 24 | area_rect = pygame.Rect(0, 0, 200, 500) 25 | 26 | def __init__(self, difficulty: Difficulty): 27 | self._difficulty = difficulty 28 | self._frame_count = 0 29 | self._game_status = GameStatus.GAME_ALIVE 30 | self._ball_served = False 31 | self._ball_served_frame = 0 32 | 33 | self._create_scene() 34 | 35 | def _create_scene(self): 36 | self._draw_group = pygame.sprite.RenderPlain() 37 | 38 | enable_slice_ball = False if self._difficulty == Difficulty.EASY else True 39 | self._ball = Ball(Scene.area_rect, enable_slice_ball, self._draw_group) 40 | self._platform_1P = Platform((80, Scene.area_rect.height - 80), 41 | Scene.area_rect, "1P", color_1P, self._draw_group) 42 | self._platform_2P = Platform((80, 50), 43 | Scene.area_rect, "2P", color_2P, self._draw_group) 44 | if self._difficulty != Difficulty.HARD: 45 | # Put the blocker at the end of the world 46 | self._blocker = Blocker(1000, Scene.area_rect, self._draw_group) 47 | else: 48 | self._blocker = Blocker(240, Scene.area_rect, self._draw_group) 49 | 50 | # Initialize the position of the ball 51 | self._ball.stick_on_platform(self._platform_1P.rect, self._platform_2P.rect) 52 | 53 | def reset(self): 54 | self._frame_count = 0 55 | self._game_status = GameStatus.GAME_ALIVE 56 | self._ball_served = False 57 | self._ball_served_frame = 0 58 | self._ball.reset() 59 | self._platform_1P.reset() 60 | self._platform_2P.reset() 61 | self._blocker.reset() 62 | 63 | # Initialize the position of the ball 64 | self._ball.stick_on_platform(self._platform_1P.rect, self._platform_2P.rect) 65 | 66 | def update(self, 67 | move_action_1P: PlatformAction, move_action_2P: PlatformAction): 68 | self._frame_count += 1 69 | self._platform_1P.move(move_action_1P) 70 | self._platform_2P.move(move_action_2P) 71 | self._blocker.move() 72 | 73 | if not self._ball_served: 74 | self._wait_for_serving_ball(move_action_1P, move_action_2P) 75 | else: 76 | self._ball_moving() 77 | 78 | if self._ball.rect.top > self._platform_1P.rect.bottom: 79 | self._game_status = GameStatus.GAME_2P_WIN 80 | elif self._ball.rect.bottom < self._platform_2P.rect.top: 81 | self._game_status = GameStatus.GAME_1P_WIN 82 | elif abs(min(self._ball.speed, key = abs)) > 40: 83 | self._game_status = GameStatus.GAME_DRAW 84 | else: 85 | self._game_status = GameStatus.GAME_ALIVE 86 | 87 | return self._game_status 88 | 89 | def _wait_for_serving_ball(self, action_1P: PlatformAction, action_2P: PlatformAction): 90 | self._ball.stick_on_platform(self._platform_1P.rect, self._platform_2P.rect) 91 | 92 | target_action = action_1P if self._ball.serve_from_1P else action_2P 93 | 94 | # Force to serve the ball after 150 frames 95 | if (self._frame_count >= 150 and 96 | target_action not in SERVE_BALL_ACTIONS): 97 | target_action = random.choice(SERVE_BALL_ACTIONS) 98 | 99 | if target_action in SERVE_BALL_ACTIONS: 100 | self._ball.serve(target_action) 101 | self._ball_served = True 102 | self._ball_served_frame = self._frame_count 103 | 104 | def _ball_moving(self): 105 | # Speed up the ball every 200 frames 106 | if (self._frame_count - self._ball_served_frame) % 100 == 0: 107 | self._ball.speed_up() 108 | 109 | self._ball.move() 110 | self._ball.check_bouncing(self._platform_1P, self._platform_2P, self._blocker) 111 | 112 | def draw_gameobjects(self, surface): 113 | self._draw_group.draw(surface) 114 | 115 | def get_scene_info(self): 116 | """ 117 | Get the scene information 118 | """ 119 | scene_info = { 120 | "frame": self._frame_count, 121 | "status": self._game_status.value, 122 | "ball": self._ball.pos, 123 | "ball_speed": self._ball.speed, 124 | "platform_1P": self._platform_1P.pos, 125 | "platform_2P": self._platform_2P.pos 126 | } 127 | 128 | if self._difficulty == Difficulty.HARD: 129 | scene_info["blocker"] = self._blocker.pos 130 | 131 | return scene_info 132 | -------------------------------------------------------------------------------- /games/arkanoid/game/gamecore.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import random 3 | 4 | from mlgame.utils.enum import StringEnum, auto 5 | 6 | from .gameobject import ( 7 | Ball, Platform, Brick, HardBrick, PlatformAction, SERVE_BALL_ACTIONS 8 | ) 9 | 10 | class Difficulty(StringEnum): 11 | EASY = auto() 12 | NORMAL = auto() 13 | 14 | class GameStatus(StringEnum): 15 | GAME_ALIVE = auto() 16 | GAME_OVER = auto() 17 | GAME_PASS = auto() 18 | 19 | class Scene: 20 | area_rect = pygame.Rect(0, 0, 200, 500) 21 | 22 | def __init__(self, difficulty, level): 23 | self._level = level 24 | self._difficulty = difficulty 25 | self._frame_count = 0 26 | self._game_status = GameStatus.GAME_ALIVE 27 | self._ball_served = False 28 | 29 | self._create_scene() 30 | 31 | def _create_scene(self): 32 | self._create_moves() 33 | self._create_bricks(self._level) 34 | 35 | def _create_moves(self): 36 | self._group_move = pygame.sprite.RenderPlain() 37 | enable_slide_ball = False if self._difficulty == Difficulty.EASY else True 38 | self._ball = Ball((93, 395), Scene.area_rect, enable_slide_ball, self._group_move) 39 | self._platform = Platform((75, 400), Scene.area_rect, self._group_move) 40 | 41 | def _create_bricks(self, level: int): 42 | def get_coordinate_and_type(string): 43 | string = string.rstrip("\n").split(' ') 44 | return int(string[0]), int(string[1]), int(string[2]) 45 | 46 | self._group_brick = pygame.sprite.RenderPlain() 47 | self._brick_container = [] 48 | 49 | import os.path 50 | dir_path = os.path.dirname(__file__) 51 | level_file_path = os.path.join(dir_path, "level_data/{0}.dat".format(level)) 52 | 53 | with open(level_file_path, 'r') as input_file: 54 | offset_x, offset_y, _ = get_coordinate_and_type(input_file.readline()) 55 | for input_pos in input_file: 56 | pos_x, pos_y, type = get_coordinate_and_type(input_pos.rstrip("\n")) 57 | BrickType = { 58 | 0: Brick, 59 | 1: HardBrick, 60 | }.get(type, Brick) 61 | 62 | brick = BrickType((pos_x + offset_x, pos_y + offset_y), 63 | self._group_brick) 64 | self._brick_container.append(brick) 65 | 66 | def reset(self): 67 | self._frame_count = 0 68 | self._game_status = GameStatus.GAME_ALIVE 69 | self._ball_served = False 70 | self._ball.reset() 71 | self._platform.reset() 72 | self._group_brick.empty() 73 | self._group_brick.add(*self._brick_container) 74 | 75 | # Reset the HP of hard bricks 76 | for brick in self._brick_container: 77 | if isinstance(brick, HardBrick): 78 | brick.reset() 79 | 80 | def update(self, platform_action: PlatformAction) -> GameStatus: 81 | self._frame_count += 1 82 | self._platform.move(platform_action) 83 | 84 | if not self._ball_served: 85 | # Force to serve the ball after 150 frames 86 | if (self._frame_count >= 150 and 87 | platform_action not in SERVE_BALL_ACTIONS): 88 | platform_action = random.choice(SERVE_BALL_ACTIONS) 89 | 90 | self._wait_for_serving_ball(platform_action) 91 | else: 92 | self._ball_moving() 93 | 94 | if len(self._group_brick) == 0: 95 | self._game_status = GameStatus.GAME_PASS 96 | elif self._ball.rect.top >= self._platform.rect.bottom: 97 | self._game_status = GameStatus.GAME_OVER 98 | else: 99 | self._game_status = GameStatus.GAME_ALIVE 100 | 101 | return self._game_status 102 | 103 | def _wait_for_serving_ball(self, platform_action: PlatformAction): 104 | self._ball.stick_on_platform(self._platform.rect.centerx) 105 | 106 | if platform_action in SERVE_BALL_ACTIONS: 107 | self._ball.serve(platform_action) 108 | self._ball_served = True 109 | 110 | def _ball_moving(self): 111 | self._ball.move() 112 | 113 | self._ball.check_hit_brick(self._group_brick) 114 | self._ball.check_bouncing(self._platform) 115 | 116 | def draw_gameobjects(self, surface): 117 | self._group_brick.draw(surface) 118 | self._group_move.draw(surface) 119 | 120 | def get_scene_info(self) -> dict: 121 | """ 122 | Get the scene information 123 | """ 124 | scene_info = { 125 | "frame": self._frame_count, 126 | "status": self._game_status.value, 127 | "ball": self._ball.pos, 128 | "platform": self._platform.pos, 129 | "bricks": [], 130 | "hard_bricks": [] 131 | } 132 | 133 | for brick in self._group_brick: 134 | if isinstance(brick, HardBrick) and brick.hp == 2: 135 | scene_info["hard_bricks"].append(brick.pos) 136 | else: 137 | scene_info["bricks"].append(brick.pos) 138 | 139 | return scene_info 140 | 141 | @property 142 | def catch_ball_times(self) -> int: 143 | return self._ball.hit_platform_times 144 | -------------------------------------------------------------------------------- /games/arkanoid/README.md: -------------------------------------------------------------------------------- 1 | # Arkanoid 2 | 3 | ## Overview 4 | 5 | 6 | 7 | At the beginning of a round, you could decide where to serve the ball by moving the platform, and the serving direction can be also decided. Try to catch the ball and break all the bricks. 8 | 9 | There are two difficulties. The `EASY` one is a simple arkanoid game, and the ball slicing mechanism is added in the `NORMAL` one, which the ball speed will be changed according to the movement of the platform. In some game level, there are "red" bricks that can be broken by hitting them twice. However, on the `NORMAL` difficulty, these red bricks can be broken by hitting them only once by speeding up the ball with the mechanism of ball slicing. 10 | 11 | ## Execution 12 | 13 | * Manual mode: `python MLGame.py -m arkanoid ` 14 | * Serve the ball to the left/right: `A`, `D` 15 | * Move the platform: `left`, `right` arrow key 16 | * ML mode: `python MLGame.py -i ml_play_template.py arkanoid ` 17 | 18 | ### Game Parameters 19 | 20 | * `difficulty`: The game style. 21 | * `EASY`: The simple arkanoid game. 22 | * `NORMAL`: The ball slicing mechanism is added. 23 | * `level_id`: Specify the level map. The available values can be checked in `game/level_data/` directory. 24 | 25 | ## Detailed Game Information 26 | 27 | ### Game Coordinate 28 | 29 | Use the coordinate system of pygame. The origin is at the top left corner of the game area, the positive direction of the x-axis is towards the right, and the positive direction of y-axis is downwards. The given coordinate of game objects is at the top left corner of the object, not at the middle of it. 30 | 31 | ### Game Area 32 | 33 | 200 \* 500 pixels 34 | 35 | ### Game Objects 36 | 37 | #### Ball 38 | 39 | * The ball is a 5-by-5-pixel blue square. 40 | * The moving speed is (±7, ±7) pixels per frame. 41 | * The ball is served from the platform, and it can be served to the left or right. If the ball is not served in 150 frames, it will be automatically served to the random direction. 42 | 43 | #### Platform 44 | 45 | * The platform is a 40-by-5-pixel green rectangle. 46 | * The moving speed is (±5, 0) pixels per frame. 47 | * The initial position is at (75, 400). 48 | 49 | #### Ball Slicing Mechanism 50 | 51 | The x speed of the ball can be changed while caught by the platform: 52 | 53 | * If the platform moves in the same direction of the ball, the x speed of the ball is increased to ±10, which is a fast ball. 54 | * If the platform is stable, the x speed of the ball is reset to ±7. 55 | * If the platform moves in the opposite direction of the ball, the ball will be hit back to the direction where it comes from, and the x speed is ±7. 56 | 57 | The mechanism is added on the `NORMAL` difficulty. 58 | 59 | #### Brick 60 | 61 | * The brick is a 25-by-10-pixel orange rectangle. 62 | * Its position is decided by the level map file. 63 | 64 | #### Hard Bricks 65 | 66 | * The hard brick is similar to the normal brick, but its color is red. 67 | * The hard brick can be broken by hitting it twice. If it is hit once, it will become a normal brick. But it can be broken by hitting it only once with the fast ball. 68 | 69 | ## Communicate with Game 70 | 71 | ### Communication Objects 72 | 73 | #### Scene Information 74 | 75 | A dictionary object sent from the game process. 76 | 77 | ``` 78 | { 79 | 'frame': 10, 80 | 'status': 'GAME_ALIVE', 81 | 'ball': (30, 332), 82 | 'platform': (30, 400), 83 | 'bricks': [(35, 50), (60, 50), (85, 50), (110, 50), (135, 50)], 84 | 'hard_bricks': [] 85 | } 86 | ``` 87 | 88 | The keys and values of the scene information: 89 | 90 | * `"frame"`: An integer, The number of the frame that this scene information is for 91 | * `"status"`: A string. The game status at this frame. It's one of the following value: 92 | * `"GAME_ALIVE"`: The game is still going. 93 | * `"GAME_PASS"`: All the bricks are broken. 94 | * `"GAME_OVER"`: The platform can't catch the ball. 95 | * `"ball"`: An `(x, y)` tuple. The position of the ball. 96 | * `"platform"`: An `(x, y)` tuple. The position of the platform. 97 | * `"bricks"`: A list storing the position of remaining normal bricks (including the hard bricks that are hit once). All elements are `(x, y)` tuples. 98 | * `"hard_bricks"`: A list storing the position of remaining hard bricks. All elements are `(x, y)` tuples. 99 | 100 | #### Game Command 101 | 102 | A string command sent to the game process for controlling the movement of the platform. 103 | 104 | ``` 105 | 'MOVE_LEFT' 106 | ``` 107 | 108 | Here are the available commands: 109 | 110 | * `"SERVE_TO_LEFT"`: Serve the ball to the left 111 | * `"SERVE_TO_RIGHT"`: Serve the ball to the right 112 | * `"MOVE_LEFT"`: Move the platform to the left 113 | * `"MOVE_RIGHT"`: Move the platform to the right 114 | * `"NONE"`: Do nothing 115 | 116 | ### Log File 117 | 118 | The name of the ml client in the log flie is `"ml"`. 119 | 120 | ## Custom Level Map Files 121 | 122 | You can define your own map file, put it in the `game/level_data/` directory, and give it an unique filename `.dat`. 123 | 124 | In the file, each line has 3 numbers representing the x, y position and the type of brick. The first line is the offset of all bricks, and the following lines are the position of bricks. Therefore, the final position of a brick is its position plus the offset. The third value in the first line is always -1. The third values in the following lines specify the type of the bricks, which 0 is a normal brick and 1 is a hard brick. For example: 125 | ``` 126 | 25 50 -1 127 | 10 0 0 128 | 35 10 0 129 | 60 20 1 130 | ``` 131 | This map file contains 3 bricks, and their positions are at (35, 50), (60, 60), (85, 70), respectively, and the third brick is a hard brick. 132 | 133 | ## About the Ball 134 | 135 | If the ball hits into another game object or the wall, the ball will be squeezed out directly to the hitting surface of that game object or the wall instead of compensating the bouncing distance. 136 | 137 | ![Imgur](https://i.imgur.com/ouk3Jzh.png) 138 | -------------------------------------------------------------------------------- /games/pingpong/game/pingpong.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from .gamecore import GameStatus, PlatformAction, Scene, color_1P, color_2P 4 | 5 | class PingPong: 6 | def __init__(self, difficulty, game_over_score: int): 7 | self._score = [0, 0] # 1P, 2P 8 | self._game_over_score = game_over_score 9 | self._scene = Scene(difficulty) 10 | 11 | self._pygame_init() 12 | 13 | def _pygame_init(self): 14 | pygame.display.init() 15 | pygame.display.set_caption("PingPong") 16 | self._surface = pygame.display.set_mode(Scene.area_rect.size) 17 | 18 | pygame.font.init() 19 | self._font = pygame.font.Font(None, 22) 20 | self._font_pos_1P = (1, self._surface.get_height() - 21) 21 | self._font_pos_2P = (1, 4) 22 | self._font_pos_speed = (self._surface.get_width() - 120, 23 | self._surface.get_height() - 21) 24 | 25 | def update(self, command): 26 | command_1P = (PlatformAction(command["ml_1P"]) 27 | if command["ml_1P"] in PlatformAction.__members__ else PlatformAction.NONE) 28 | command_2P = (PlatformAction(command["ml_2P"]) 29 | if command["ml_2P"] in PlatformAction.__members__ else PlatformAction.NONE) 30 | 31 | game_status = self._scene.update(command_1P, command_2P) 32 | self._draw_screen() 33 | 34 | if game_status != GameStatus.GAME_ALIVE: 35 | print(game_status.value) 36 | if self._game_over(game_status): 37 | self._print_result() 38 | return "QUIT" 39 | 40 | return "RESET" 41 | 42 | def _draw_screen(self): 43 | """ 44 | Draw the scene to the display 45 | """ 46 | self._surface.fill((0, 0, 0)) 47 | self._scene.draw_gameobjects(self._surface) 48 | 49 | font_surface_1P = self._font.render( 50 | "1P: {}".format(self._score[0]), True, color_1P) 51 | font_surface_2P = self._font.render( 52 | "2P: {}".format(self._score[1]), True, color_2P) 53 | font_surface_speed = self._font.render( 54 | "Speed: {}".format(self._scene._ball.speed), True, (255, 255, 255)) 55 | self._surface.blit(font_surface_1P, self._font_pos_1P) 56 | self._surface.blit(font_surface_2P, self._font_pos_2P) 57 | self._surface.blit(font_surface_speed, self._font_pos_speed) 58 | 59 | pygame.display.flip() 60 | 61 | def _game_over(self, status): 62 | """ 63 | Check if the game is over 64 | """ 65 | if status == GameStatus.GAME_1P_WIN: 66 | self._score[0] += 1 67 | elif status == GameStatus.GAME_2P_WIN: 68 | self._score[1] += 1 69 | else: # Draw game 70 | self._score[0] += 1 71 | self._score[1] += 1 72 | 73 | is_game_over = (self._score[0] == self._game_over_score or 74 | self._score[1] == self._game_over_score) 75 | 76 | return is_game_over 77 | 78 | def _print_result(self): 79 | """ 80 | Print the result 81 | """ 82 | if self._score[0] > self._score[1]: 83 | win_side = "1P" 84 | elif self._score[0] == self._score[1]: 85 | win_side = "No one" 86 | else: 87 | win_side = "2P" 88 | 89 | print("{} wins! Final score: {}-{}".format(win_side, *self._score)) 90 | 91 | def reset(self): 92 | """ 93 | Reset the game 94 | """ 95 | self._scene.reset() 96 | 97 | def get_player_scene_info(self): 98 | """ 99 | Get the scene information to be sent to the player 100 | """ 101 | scene_info = self._scene.get_scene_info() 102 | return {"ml_1P": scene_info, "ml_2P": scene_info} 103 | 104 | def get_keyboard_command(self): 105 | """ 106 | Get the command according to the pressed keys 107 | """ 108 | key_pressed_list = pygame.key.get_pressed() 109 | 110 | if key_pressed_list[pygame.K_PERIOD]: cmd_1P = "SERVE_TO_LEFT" 111 | elif key_pressed_list[pygame.K_SLASH]: cmd_1P = "SERVE_TO_RIGHT" 112 | elif key_pressed_list[pygame.K_LEFT]: cmd_1P = "MOVE_LEFT" 113 | elif key_pressed_list[pygame.K_RIGHT]: cmd_1P = "MOVE_RIGHT" 114 | else: cmd_1P = "NONE" 115 | 116 | if key_pressed_list[pygame.K_q]: cmd_2P = "SERVE_TO_LEFT" 117 | elif key_pressed_list[pygame.K_e]: cmd_2P = "SERVE_TO_RIGHT" 118 | elif key_pressed_list[pygame.K_a]: cmd_2P = "MOVE_LEFT" 119 | elif key_pressed_list[pygame.K_d]: cmd_2P = "MOVE_RIGHT" 120 | else: cmd_2P = "NONE" 121 | 122 | return {"ml_1P": cmd_1P, "ml_2P": cmd_2P} 123 | 124 | def get_game_info(self): 125 | return { 126 | "scene": { 127 | "size": [200, 500], 128 | }, 129 | "game_object": [ 130 | { "name": "platform_1P", "size": [40, 30], "color": [84, 149, 255] }, 131 | { "name": "platform_2P", "size": [40, 30], "color": [219, 70, 92] }, 132 | { "name": "blocker", "size": [30, 20], "color": [213, 224, 0] }, 133 | { "name": "ball", "size": [5, 5], "color": [66, 226, 126] }, 134 | ] 135 | } 136 | 137 | def get_game_progress(self): 138 | scene_info = self._scene.get_scene_info() 139 | 140 | return { 141 | "status": { 142 | "ball_speed": scene_info["ball_speed"], 143 | }, 144 | "game_object": { 145 | "ball": [scene_info["ball"]], 146 | "platform_1P": [scene_info["platform_1P"]], 147 | "platform_2P": [scene_info["platform_2P"]], 148 | } 149 | } 150 | 151 | def get_game_result(self): 152 | scene_info = self._scene.get_scene_info() 153 | 154 | if self._score[0] > self._score[1]: 155 | status = ["GAME_PASS", "GAME_OVER"] 156 | elif self._score[0] < self._score[1]: 157 | status = ["GAME_OVER", "GAME_PASS"] 158 | else: 159 | status = ["GAME_DRAW", "GAME_DRAW"] 160 | 161 | return { 162 | "frame_used": scene_info["frame"], 163 | "result": status, 164 | "ball_speed": scene_info["ball_speed"], 165 | } 166 | -------------------------------------------------------------------------------- /mlgame/crosslang/DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Documentation of `crosslang` Module 2 | 3 | ## Table of Contents 4 | 5 | * [How It Works](#how-it-works) 6 | * [ML Scripts](#ml-scripts) 7 | * [Compilation](#compilation) 8 | * [Execution](#execution) 9 | * [How to Support Another Language](#how-to-support-another-language) 10 | * [Generate Execution Command](#generate-execution-command) 11 | * [Language API](#language-api) 12 | 13 | ## How It Works 14 | 15 | ### ML Scripts 16 | 17 | The non-python script for the ml client is also placed in the `ml` directory of the game. 18 | 19 | ### Compilation 20 | 21 | 22 | 23 | If the user specifies a non-python script, MLGame will: 24 | 1. Invoke `mlgame.crosslang.main` module to handle the script. 25 | 2. `mlgame.crosslang.main` module will check the file extension of the script, and find corresponding programming language `lang` in `EXTENSION_LANG_MAP` defined in `ext_lang.map.py`. 26 | 3. Invoke `compile_script()` defined in the module `mlgame.crosslang.compile..main` to compile the script. For example, if the user specifies `ml_play.cpp`, the `mlgame.crosslang.compile.cpp.main` module is used. 27 | 4. The full path of the non-python script is passed to the `compile_script()`, and then the function will preprocess the script, link libraries, compile, and return the execution command in a list object. 28 | 29 | ### Execution 30 | 31 | 32 | 33 | After compilation, `mlgame.crosslang.ml_play` module runs in the ml process as the proxy of the non-python client. It summons a subprocess to execute the client, and runs as communication bridge between the game and the client. The I/O of the client is redirected to the module, therefore, the client can receive and send messages via basic I/O. For example, `std::cout` and `std::cin` for C++. 34 | 35 | The message sent between the module and the client is a header string following a JSON string. The language API handles the message and convert it to the usable object for the user program to execute. 36 | 37 | ## How to Support Another Language 38 | 39 | ### Generate Execution Command 40 | 41 | 1. Create a directory in `mlgame/crosslang/compile` and name it to the language name 42 | 2. Create `__init__.py` and `main.py` in the new directory 43 | 3. Edit `main.py` with the following code: 44 | 45 | ```python 46 | from pathlib import Path 47 | from mlgame.crosslang.exceptions import CompilationError 48 | 49 | def compile_script(script_full_path): 50 | 51 | # Code for handling the source code # 52 | 53 | # Raise CompilationError if an error occurred 54 | if error_occurred: 55 | raise CompilationError(Path(script_full_path).name, reason) 56 | 57 | # Generate the command segments stored in a list object # 58 | return ["commnd", "path/to/file", "--foo", "--bar"] 59 | ``` 60 | The method `compile_script` is used for handling the source code, and `script_full_path` is a string storing the full path to the code. If it will generate additional files, it is recommended that put them in the same directory as the source code. The returned object is a list storing the execution command. 61 | 62 | 4. Edit `mlgame/crosslang/ext_lang_map.py` and add the "file extension-language" pair (".js": "javascript" for example) to the dictionary `EXTENSION_LANG_MAP` 63 | 64 | ### Language API 65 | 66 | The language API provides an interface for the user to get the object sent from the game or send the object back to the game. The object will be converted to the string format and it will be sent or received via standard I/O. The string format is a header string following a JSON string. 67 | 68 | #### Methods 69 | 70 | There are 5 functions to be implemented: 71 | 72 | **Get the object sent from the game** 73 | 74 | * `get_init_args()`: Get the initial arguments. It will return an usable object. 75 | * Received string format: `"__init__ {"args": [...], "kwargs": {...}}\n"` 76 | * `get_scene_info()`: Get the scene information from the game. It will return an usable object. 77 | * Received string format: `"__scene_info__ \n"`. `scene_info` is a dictionary object sent from the game. 78 | 79 | For example: 80 | ```c++ 81 | json get_scene_info() 82 | { 83 | string scene_info_str; 84 | getline(cin, scene_info_str); 85 | 86 | // Ignore header "__scene_info__" 87 | scene_info_str.erase(0, scene_info_str.find_first_of('{')); 88 | 89 | return json::parse(scene_info_str); 90 | } 91 | ``` 92 | 93 | **Send the object back to the game** 94 | 95 | * `client_ready()`: Send the ready command to the game. 96 | * Sent string format: `"__command__ READY\n"` 97 | * `client_reset()`: Send the reset command to the game. 98 | * Sent string format: `"__command__ RESET\n"` 99 | * `send_command(command)`: Send the game command to the game. `command` parameter will get an usable object which will be converted to JSON string in the function. 100 | * Sent string format: `"__command__ \n"`. 101 | 102 | For example: 103 | ```c++ 104 | void client_reset() 105 | { 106 | cout << "__command__ RESET" << endl; 107 | } 108 | ``` 109 | 110 | The message received from the client without `"__command__"` header will be printed out directly. 111 | 112 | **>>> Important! <<<** All the message sent from the client must contain a newline character ("\n") at the end. And then flush the message from stdout each time, otherwise, the message will be left in the buffer, and the moudle won't receive it until the buffer is full. For example, in C++, always use `std::endl` at the end of `std::cout`, which will add a newline character and flush the message. 113 | 114 | #### Simulate `MLPlay` 115 | 116 | You could provide a class like `MLPlay` to the user, and write a loop to interact with language API and user's `MLPlay`. There are 3 member functions of `MLPlay`: 117 | 118 | * `constructor(init_args)`: Do initialization jobs. `init_args` is the initial arguments sent from the game. 119 | * `update(scene_info) -> [game|reset]command`: Handle the scene information and generate the command. 120 | * The returned command could be game command or the reset command (could be a `"RESET"` for example) 121 | * `reset()`: Do reset jobs. 122 | 123 | The flow chart of the loop: 124 | 125 | 126 | 127 | For example: 128 | ```c++ 129 | int main() 130 | { 131 | AbstractMLPlay *ml = new MLPlay(get_init_args()); 132 | client_ready(); 133 | 134 | json command; 135 | while (1) { 136 | command = ml->update(get_scene_info()); 137 | if (command == "RESET") { 138 | client_reset(); 139 | ml->reset(); 140 | client_ready(); 141 | continue; 142 | } 143 | send_command(command); 144 | } 145 | 146 | delete ml; 147 | return 0; 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /games/pingpong/README.md: -------------------------------------------------------------------------------- 1 | # PingPong 2 | 3 | **Game version: 1.1** 4 | 5 | ## Overview 6 | 7 | 8 | 9 | At the beginning of a round, you could move the platform to decide where to serve the ball and its direction. If the ball is not served in 150 frames, it will be randomly served from the platform. The ball speed starts from 7, and is increased every 100 frames. If the ball speed exceeds 40, this round is a draw game. 10 | 11 | There are two mechanisms. One is ball slicing: The x speed of the ball will be changed according to the movement of the platform while the platform catches the ball. The other is a moving blocker at the middle of the game place. 12 | 13 | ## Execution 14 | 15 | * Manual mode: `python MLGame.py -m pingpong [game_over_score]` 16 | * Serve the ball to the left/right: 1P - `.`, `/`; 2P - `Q`, `E` 17 | * Move the platform: 1P - `left`, `right` arrow keys; 2P - `A`, `D` 18 | * ML mode: `python MLGame.py -i ml_play_template.py pingpong [game_over_score]` 19 | 20 | ### Game Parameters 21 | 22 | * `difficulty`: The game style. There are 3 difficulties. 23 | * `EASY`: The simple pingpong game. 24 | * `NORMAL`: The ball slicing mechanism is added. 25 | * `HARD`: The ball slicing and the blocker mechanism are added. 26 | * `game_over_score`: [Optional] When the score of either side reaches this value, the game will be exited. The default value is 3. If the `-1` flag is set, it will be 1. 27 | 28 | ## Detailed Game Information 29 | 30 | ### Game Coordinate 31 | 32 | Same as the game "Arkanoid" 33 | 34 | ### Game Area 35 | 36 | 500 \* 200 pixels 37 | 38 | 1P side is at the lower half of the game area, and 2P side is at the upper half of the game area. 39 | 40 | ### Game Objects 41 | 42 | #### Ball 43 | 44 | * The ball is a 5-by-5-pixel green square. 45 | * The ball will be served from the 1P side first, and then change side for each round. 46 | * The ball is served from the platform, and it can be served to the left or right. If the ball is not served in 150 frames, it will be automatically served to the random direction. 47 | * The initial moving speed is (±7, ±7) pixels per frame, and it is increased every 100 frames after the ball is served. 48 | 49 | #### Platform 50 | 51 | * The platform is a 40-by-30-pixel rectangle. 52 | * The color of the 1P platform is red, and it of the 2P platform is blue. 53 | * The moving speed is (±5, 0) pixels per frame. 54 | * The initial position of the 1P platform is at (80, 420), and it of the 2P platform is at (80, 50). 55 | 56 | #### Ball Slicing Mechanism 57 | 58 | The x speed of the ball is changed according to the movement of the platform while it catches the ball. 59 | 60 | * If the platform moves in the same direction of the ball, the x speed of the ball is increased by 3 (only once). 61 | * If the platform is stable, the x speed of the ball is reset to current basic ball speed. 62 | * If the platform moves in the opposite direction of the ball, the ball will be hit back to the direction where it comes from and the x speed is reset to the current basic ball speed. 63 | 64 | The ball slicing mechanism is added on `NORMAL` and `HARD` difficulties. 65 | 66 | #### Blocker 67 | 68 | * The blocker is a 30-by-20-pixel rectangle. 69 | * The initial position of x is randomly choiced from 0 to 180, 20 per step, and the initial position of y is 240. The moving speed is (±5, 0) pixels per frame. 70 | * The blocker will keep moving left and right. The initial direction is random. 71 | * The blocker doesn't have ball slicing mechanism, which the ball speed is the same after it hits the blocker. 72 | 73 | The blocker is added on `HARD` difficulty. 74 | 75 | ## Communicate with Game 76 | 77 | The example script is in [`ml_play_template.py`](./ml/ml_play_template.py). 78 | 79 | ### Initial arguments 80 | 81 | * `side`: A string which is either `"1P"` or `"2P"` to indicate that the script is used by which side. 82 | 83 | ### Communication Objects 84 | 85 | #### Scene Information 86 | 87 | A dictionary object sent from the game process. 88 | 89 | ``` 90 | { 91 | 'frame': 42, 92 | 'status': 'GAME_ALIVE', 93 | 'ball': (189, 128), 94 | 'ball_speed': (7, -7), 95 | 'platform_1P': (0, 420), 96 | 'platform_2P': (0, 50), 97 | 'blocker': (50, 240) 98 | } 99 | ``` 100 | 101 | The keys and values of the scene information: 102 | 103 | * `"frame"`: An integer. The number of frame that this scene information is for 104 | * `"status"`: A string. The game status at this frame. It's one of the following 4 statuses: 105 | * `"GAME_ALIVE"`: This round is still going. 106 | * `"GAME_1P_WIN"`: 1P wins this round. 107 | * `"GAME_2P_WIN"`: 2P wins this round. 108 | * `"GAME_DRAW"`: This round is a draw game. 109 | * `"ball"`: An `(x, y)` tuple. The position of the ball. 110 | * `"ball_speed"`: An `(x, y)` tuple. The speed of the ball. 111 | * `"platform_1P"`: An `(x, y)` tuple. The position of the 1P platform. 112 | * `"platform_2P"`: An `(x, y)` tuple. The position of the 2P platform. 113 | * `"blocker"`: An `(x, y)` tuple. The position of the blocker. If the game is not on `HARD` difficulty, this field is `None`. 114 | 115 | #### Game Command 116 | 117 | A string command sent to the game process for controlling the platform. 118 | 119 | ``` 120 | 'MOVE_RIGHT' 121 | ``` 122 | 123 | Here are available commands: 124 | 125 | * `"SERVE_TO_LEFT"`: Serve the ball to the left. 126 | * `"SERVE_TO_RIGHT"`: Serve the ball to the right. 127 | * `"MOVE_LEFT"`: Move the platform to the left. 128 | * `"MOVE_RIGHT"`: Move the platform to the right. 129 | * `"NONE"`: Do nothing. 130 | 131 | ### Log File 132 | 133 | The name of the ml client in the log flie is `"ml_1P"` for 1P player and `"ml_2P"` for 2P player. 134 | 135 | ## Input Scripts for ML Mode 136 | 137 | The pingpong game is a 2P game, so it can accept two different ml scripts by specifying `-i -i `. If there is only one script specified, 1P and 2P will use the same script. 138 | 139 | You can specify `ml_play_manual.py` as the input script. It will create an invisible joystick for you to play with the ml process. For example: 140 | 1. Start the game by the command `python MLGame.py -i ml_play_template.py -i ml_play_manual.py pingpong `. There will be 2 windows, and one is the "Invisible joystick". The terminal will output a message "Invisible joystick is used. Press Enter to start the 2P ml process." 141 | 142 | 143 | 144 | 2. Drag the invisible joystick aside, and focus on it (The color of the window title is not gray). 145 | 146 | 147 | 148 | 3. Press Enter key to start the game, and use left and right arrow keys to control the platform of the selected side. 149 | 150 | ## About the Ball 151 | 152 | The behavior of the ball is the same as the game "Arkanoid". 153 | -------------------------------------------------------------------------------- /mlgame/execution_command.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, REMAINDER 2 | from enum import Enum, auto 3 | from pathlib import Path 4 | import re 5 | 6 | from ._version import version 7 | from .exceptions import ExecutionCommandError 8 | 9 | def get_command_parser(): 10 | """ 11 | Generate an ArgumentParser for parse the arguments in the command line 12 | """ 13 | usage_str = ("python %(prog)s [options] [game_params]") 14 | description_str = ("A platform for applying machine learning algorithm " 15 | "to play pixel games. " 16 | "In default, the game runs in the machine learning mode. ") 17 | 18 | parser = ArgumentParser(usage = usage_str, description = description_str, 19 | add_help = False) 20 | 21 | parser.add_argument("game", type = str, nargs = "?", 22 | help = "the name of the game to be started") 23 | parser.add_argument("game_params", nargs = REMAINDER, default = None, 24 | help = "[optional] the additional settings for the game. " 25 | "Note that all arguments after will be collected to 'game_params'.") 26 | 27 | group = parser.add_argument_group(title = "functional options") 28 | group.add_argument("--version", action = "version", version = version) 29 | group.add_argument("-h", "--help", action = "store_true", 30 | help = "show this help message and exit. " 31 | "If this flag is specified after the , " 32 | "show the help message of the game instead.") 33 | group.add_argument("-l", "--list", action = "store_true", dest = "list_games", 34 | help = "list available games. If the game in the 'games' directory " 35 | "provides 'config.py' which can be loaded, it will be listed.") 36 | 37 | group = parser.add_argument_group(title = "game execution options", 38 | description = "Game execution options must be specified before arguments.") 39 | group.add_argument("-f", "--fps", type = int, default = 30, 40 | help = "the updating frequency of the game process [default: %(default)s]") 41 | group.add_argument("-m", "--manual-mode", action = "store_true", 42 | help = "start the game in the manual mode instead of " 43 | "the machine learning mode [default: %(default)s]") 44 | group.add_argument("-r", "--record", action = "store_true", dest = "record_progress", 45 | help = "pickle the game progress (a list of SceneInfo) to the log file. " 46 | "One file for a round, and stored in '/log/' directory. " 47 | "[default: %(default)s]") 48 | group.add_argument("-1", "--one-shot", action = "store_true", dest = "one_shot_mode", 49 | help = "quit the game when the game is passed or is over. " 50 | "Otherwise, the game will restart automatically. [default: %(default)s]") 51 | group.add_argument("-i", "--input-script", type = str, action = "append", 52 | default = None, metavar = "SCRIPT", 53 | help = "specify user script(s) for the machine learning mode. " 54 | "For multiple user scripts, use this flag multiple times. " 55 | "The script path starts from 'games//ml/' directory. " 56 | "'-i ml_play.py' means the script path is 'games//ml/ml_play.py', and " 57 | "'-i foo/ml_play.py' means the script path is 'games/ml/foo/ml_play.py'. " 58 | "If the script is in the subdirectory of the 'ml' directory, make sure the " 59 | "subdirectory has '__init__.py' file.") 60 | 61 | return parser 62 | 63 | class GameMode(Enum): 64 | """ 65 | The mode of the game 66 | """ 67 | __slots__ = () 68 | 69 | MANUAL = auto() 70 | ML = auto() 71 | 72 | class ExecutionCommand: 73 | """ 74 | The data class for storing the command of the game execution 75 | 76 | @var game_name The name of the game to be executed 77 | @var game_params A list of parameters for the game 78 | @var one_shot_mode Whether to execute the game for only once 79 | @var game_mode The mode of the game to be executed. 80 | It will be one of attributes of `GameMode`. 81 | @var record_progress Whether to record the game progress 82 | @var fps The FPS of the game 83 | @var input_modules A list of user modules for running the ML mode 84 | """ 85 | 86 | def __init__(self, parsed_args): 87 | """ 88 | Generate the game configuration from the parsed command line arguments 89 | """ 90 | self.game_name = parsed_args.game 91 | self.game_params = parsed_args.game_params 92 | 93 | self.game_mode = GameMode.MANUAL if parsed_args.manual_mode else GameMode.ML 94 | self.one_shot_mode = parsed_args.one_shot_mode 95 | self.record_progress = parsed_args.record_progress 96 | 97 | self.fps = parsed_args.fps 98 | 99 | self.input_modules = self._parse_ml_scripts(parsed_args.input_script) 100 | if self.game_mode == GameMode.ML and len(self.input_modules) == 0: 101 | raise ExecutionCommandError("No script or module is specified. " 102 | "Cannot start the game in the machine learning mode.") 103 | 104 | def _parse_ml_scripts(self, input_scripts): 105 | """ 106 | Check whether the provided input scripts are all existing or not 107 | 108 | If it passes, the name of scripts is converted to the absolute import path and 109 | return a list of them. 110 | Otherwise, raise the ExecutionCommandError. 111 | """ 112 | if not input_scripts: 113 | return [] 114 | 115 | top_dir_path = Path(__file__).parent.parent 116 | module_list = [] 117 | 118 | for script_name in input_scripts: 119 | local_script_path = Path("games", self.game_name, "ml", script_name) 120 | full_script_path = top_dir_path / local_script_path 121 | 122 | if not full_script_path.exists(): 123 | raise ExecutionCommandError( 124 | "The script '{}' does not exist. " 125 | "Cannot start the game in the machine learning mode." 126 | .format(local_script_path)) 127 | 128 | # If the assigned script is not a python file, 129 | # pack the crosslang client and the script into a tuple for futher handling. 130 | if full_script_path.suffix != ".py": 131 | module_list.append(("mlgame.crosslang.ml_play", full_script_path.__str__())) 132 | else: 133 | # Replace the file path seperator with the dot 134 | sub_module = re.sub(r'[\\/]', r'.', script_name) 135 | module_list.append("games.{}.ml.{}" 136 | .format(self.game_name, sub_module.split('.py')[0])) 137 | 138 | return module_list 139 | 140 | def __str__(self): 141 | return ("{" + 142 | "'game_name': '{}', ".format(self.game_name) + 143 | "'game_params': {}, ".format(self.game_params) + 144 | "'game_mode': {}, ".format(self.game_mode) + 145 | "'one_shot_mode': {}, ".format(self.one_shot_mode) + 146 | "'record_progress': {}, ".format(self.record_progress) + 147 | "'fps': {}, ".format(self.fps) + 148 | "'input_modules': {}".format(self.input_modules) + 149 | "}") 150 | -------------------------------------------------------------------------------- /mlgame/execution.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse the execution command, load the game config, and execute the game 3 | """ 4 | import importlib 5 | import os 6 | import os.path 7 | import sys 8 | 9 | from .crosslang.main import compile_script 10 | from .crosslang.exceptions import CompilationError 11 | from .execution_command import get_command_parser, GameMode, ExecutionCommand 12 | from .exceptions import ExecutionCommandError, GameConfigError 13 | from .gameconfig import GameConfig 14 | from .loops import GameMLModeExecutorProperty, MLExecutorProperty 15 | from .utils.argparser_generator import get_parser_from_dict 16 | from . import errno 17 | 18 | def execute(): 19 | """ 20 | Parse the execution command and execute the game 21 | """ 22 | try: 23 | execution_cmd, game_config = _parse_command_line() 24 | except (ExecutionCommandError, GameConfigError) as e: 25 | print("Error:", e) 26 | sys.exit(errno.COMMAND_LINE_ERROR) 27 | 28 | if execution_cmd.game_mode == GameMode.MANUAL: 29 | _run_manual_mode(execution_cmd, game_config.game_setup) 30 | else: 31 | _run_ml_mode(execution_cmd, game_config.game_setup) 32 | 33 | def _parse_command_line(): 34 | """ 35 | Parse the command line arguments 36 | 37 | If "-h/--help" or "-l/--list" flag is specfied, it will print the related message 38 | and exit the program. 39 | 40 | @return A tuple of (`ExecutionCommand` object, `GameConfig` object) 41 | """ 42 | # Parse the command line arguments 43 | cmd_parser = get_command_parser() 44 | parsed_args = cmd_parser.parse_args() 45 | 46 | ## Functional print ## 47 | # If "-h/--help" is specified, print help message and exit. 48 | if parsed_args.help: 49 | cmd_parser.print_help() 50 | sys.exit(0) 51 | # If "-l/--list" is specified, list available games and exit. 52 | elif parsed_args.list_games: 53 | _list_games() 54 | sys.exit(0) 55 | 56 | # Load the game defined config 57 | game_config = GameConfig(parsed_args.game) 58 | 59 | # Create game_param parser 60 | param_parser = get_parser_from_dict(game_config.game_params) 61 | parsed_game_params = param_parser.parse_args(parsed_args.game_params) 62 | 63 | # Replace the input game_params with the parsed one 64 | parsed_args.game_params = [value for value in vars(parsed_game_params).values()] 65 | 66 | # Generate execution command 67 | try: 68 | exec_cmd = ExecutionCommand(parsed_args) 69 | except ExecutionCommandError: 70 | raise 71 | 72 | return exec_cmd, game_config 73 | 74 | def _list_games(): 75 | """ 76 | List available games which provide "config.py" in the game directory. 77 | """ 78 | game_root_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "games") 79 | dirs = [f for f in os.listdir(game_root_dir) 80 | if ("__" not in f) and (os.path.isdir(os.path.join(game_root_dir, f)))] 81 | 82 | game_info_list = [("Game", "Version"), ("-----", "-----")] 83 | max_name_len = 5 84 | # Load the config and version 85 | for game_dir in dirs: 86 | try: 87 | game_defined_config = importlib.import_module( 88 | "games.{}.config".format(game_dir)) 89 | game_version = game_defined_config.GAME_VERSION 90 | except ModuleNotFoundError: 91 | continue 92 | except AttributeError: 93 | game_version = "" 94 | 95 | game_info_list.append((game_dir, game_version)) 96 | max_name_len = max(max_name_len, len(game_dir)) 97 | 98 | for name, version in game_info_list: 99 | print(name.ljust(max_name_len + 1), version) 100 | 101 | def _run_manual_mode(execution_cmd: ExecutionCommand, game_setup): 102 | """ 103 | Execute the game specified in manual mode 104 | 105 | @param execution_cmd The `ExecutionCommand` object 106 | @param game_setup The `GAME_SETUP` defined in the game config 107 | """ 108 | from .loops import GameManualModeExecutor 109 | from .exceptions import GameProcessError 110 | 111 | # Collect ml names 112 | ml_names = [] 113 | for client in game_setup["ml_clients"]: 114 | ml_names.append(client["name"]) 115 | 116 | game_cls = game_setup["game"] 117 | try: 118 | executor = GameManualModeExecutor(execution_cmd, game_cls, ml_names) 119 | executor.start() 120 | except GameProcessError as e: 121 | print("Error: Exception occurred in 'game' process:") 122 | print(e.message) 123 | sys.exit(errno.GAME_EXECUTION_ERROR) 124 | 125 | def _run_ml_mode(execution_cmd: ExecutionCommand, game_setup): 126 | """ 127 | Execute the game specified in ml mode 128 | 129 | @param execution_cmd The `ExecutionCommand` object 130 | @param game_setup The `GAME_SETUP` defined in the game config 131 | """ 132 | from .process import ProcessManager 133 | 134 | game_propty = _get_game_executor_propty(execution_cmd, game_setup) 135 | ml_propties = _get_ml_executor_propties(execution_cmd, game_setup) 136 | 137 | process_manager = ProcessManager(game_propty, ml_propties) 138 | returncode = process_manager.start() 139 | if returncode == -1: 140 | sys.exit(errno.GAME_EXECUTION_ERROR) 141 | 142 | def _get_game_executor_propty( 143 | execution_cmd: ExecutionCommand, game_setup) -> GameMLModeExecutorProperty: 144 | """ 145 | Get the property for the game executor in the ml mode 146 | """ 147 | game_cls = game_setup["game"] 148 | ml_clients = game_setup["ml_clients"] 149 | ml_names = [] 150 | for client in ml_clients: 151 | ml_names.append(client["name"]) 152 | 153 | return GameMLModeExecutorProperty( 154 | "game", execution_cmd, game_cls, ml_names) 155 | 156 | def _get_ml_executor_propties(execution_cmd: ExecutionCommand, game_setup) -> list: 157 | """ 158 | Get the property for the ml executors 159 | 160 | @return A list of generated properties 161 | """ 162 | propties = [] 163 | ml_clients = game_setup["ml_clients"] 164 | dynamic_ml_clients = game_setup["dynamic_ml_clients"] 165 | 166 | for i in range(len(ml_clients)): 167 | ml_client = ml_clients[i] 168 | 169 | ml_name = ml_client["name"] 170 | args = ml_client.get("args", ()) 171 | kwargs = ml_client.get("kwargs", {}) 172 | 173 | # Assign the input modules to the ml processes 174 | if dynamic_ml_clients and i == len(execution_cmd.input_modules): 175 | # If 'dynamic_ml_client' is set, then the number of ml clients 176 | # is decided by the number of input modules. 177 | break 178 | else: 179 | # If the number of provided modules is less than the number of processes, 180 | # the last module is assigned to the rest processes. 181 | module_id = (i if i < len(execution_cmd.input_modules) 182 | else len(execution_cmd.input_modules) - 1) 183 | ml_module = execution_cmd.input_modules[module_id] 184 | 185 | # Compile the non-python script 186 | # It is stored as a (crosslang ml client module, non-python script) tuple. 187 | if isinstance(ml_module, tuple): 188 | non_py_script = ml_module[1] 189 | ml_module = ml_module[0] 190 | # Wrap arguments passed to be passed to the script 191 | module_kwargs = { 192 | "script_execution_cmd": _compile_non_py_script(non_py_script), 193 | "init_args": args, 194 | "init_kwargs": kwargs 195 | } 196 | args = () 197 | kwargs = module_kwargs 198 | 199 | propties.append(MLExecutorProperty(ml_name, ml_module, args, kwargs)) 200 | 201 | return propties 202 | 203 | def _compile_non_py_script(script_path): 204 | """ 205 | Compile the non-python script and return the execution command for the script 206 | 207 | @return A list of command segments for executing the compiled script 208 | """ 209 | try: 210 | print("Compiling '{}'...".format(script_path), end = " ", flush = True) 211 | script_execution_cmd = compile_script(script_path) 212 | except CompilationError as e: 213 | print("Failed\nError: {}".format(e)) 214 | sys.exit(errno.COMPILATION_ERROR) 215 | print("OK") 216 | 217 | return script_execution_cmd 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MLGame 2 | 3 | A platform for applying machine learning algorithm to play pixel games 4 | 5 | MLGame separates the machine learning part from the game core, which makes users easily apply codes to play the game. (Support non-python script as the client. Check [here](mlgame/crosslang/README.md) for more information.) 6 | 7 | For the concept and the API of the MLGame, visit the [wiki page](https://github.com/LanKuDot/MLGame/wiki) of this repo (written in Traditional Chinese). 8 | 9 | ## Requirements 10 | 11 | * Python 3.6+ 12 | * pygame==1.9.6+ 13 | * pygame==2.0.0 if installs on mac 14 | * Other machine learning libraries you needed 15 | 16 | ## Usage 17 | 18 | ``` 19 | $ python MLGame.py [options] [game_params] 20 | ``` 21 | 22 | * `game`: The name of the game to be started. Use `-l` flag to list available games. 23 | * `game_params`: The additional parameters for the game. Use `python MLGame.py -h` to list game parameters of a game. 24 | * Note that all arguments after `` will be collected to this paremeter 25 | * functional options: 26 | * `--version`: Show the version number 27 | * `-h`: Show the help message 28 | * `-l`: List available games 29 | * game execution options: 30 | * `-f FPS`: Specify the updating frequency of the game 31 | * `-m`: Play the game in the manual mode (as a normal game) 32 | * `-1`: Quit the game when the game is over or is passed. Otherwise, the game will restart automatically. 33 | * `-r`: Pickle the game progress (a list of "SceneInfo") to log files. 34 | * `-i SCRIPT [-i SCRIPT ...]`: Specify the script used in the machine learning mode. For multiple scripts, use this flag multiple times. 35 | The script path starts from `games//ml/` direcotry. `-i ml_play.py` means the file is at `games//ml/ml_play.py`, and `-i foo/ml_play.py` means the file is at `games//ml/foo/ml_play.py`. If the file is in the subdirectory of the `ml` directory, make sure that the subdirectory has a `__init__.py` file. 36 | 37 | **Game execution options must be specified before <game> arguments.** Use `python MLGame.py -h` for more information. 38 | 39 | For example: 40 | 41 | * List available games: 42 | ``` 43 | $ python MLGame.py -l 44 | ``` 45 | 46 | * List game parameters of the game arkanoid: 47 | ``` 48 | $ python MLGame.py arkanoid -h 49 | ``` 50 | 51 | * Play the game arkanoid level 3 in manual mode on easy difficulty with 45 fps 52 | ``` 53 | $ python MLGame.py -m -f 45 arkanoid EASY 3 54 | ``` 55 | 56 | * Play the game arkanoid level 2 on normal difficulty, record the game progress, and specify the script `ml_play_template.py` 57 | 58 | ``` 59 | $ python MLGame.py -r -i ml_play_template.py arkanoid NORMAL 2 60 | ``` 61 | 62 | ## Play the Game 63 | 64 | In default, the game is executed in the machine learning mode. You could play the game in the manual mode by specifying `-m` flag. 65 | 66 | In the machine learning mode, you have to provide the script to play the game, which is put in the `games//ml` directory. For example, if there is a file `ml_play.py` in the `games/arkanoid/ml` directory, by specifying the `-i ml_play.py` in the command to use that file to play the game `arkanoid`. 67 | 68 | The games in this repository provide `ml_play_template.py` in their `ml` directory, which contains simple code for playing the game. You could duplicate the script and modify it, or use this file for the first time execution. 69 | 70 | ### Read Instruction 71 | 72 | The game provides README files for detailed information, such as: 73 | 74 | * How to execute and play the game 75 | * The information of game objects 76 | * The format of the scene information and the game command 77 | 78 | Here are README of games: 79 | 80 | * [arkanoid](games/arkanoid/README.md) 81 | * [pingpong](games/pingpong/README.md) 82 | * [snake](games/snake/README.md) 83 | 84 | ### `MLPlay` class 85 | 86 | The scripts for playing the game must have a `MLPlay` class and provide the corresponding functions. Here is a template of the `MLPlay` class: 87 | 88 | ```python 89 | class MLPlay: 90 | def __init__(self, init_arg_1, init_arg_2, ...): 91 | ... 92 | 93 | def update(self, scene_info): 94 | ... 95 | 96 | def reset(self): 97 | ... 98 | ``` 99 | 100 | * `__init__(self, init_arg_1, init_arg_2, ...)`: The initialization of `MLPlay` class, such as loading trained module or initializing the member variables 101 | * `init_arg_x`: The initial arguments sent from the game. 102 | * `update(self, scene_info) -> command or "RESET"`: Handle the received scene information and generate the game command 103 | * `scene_info`: The scene information sent from the game. 104 | * `command`: The game command sent back to the game. 105 | * If the `scene_info` contains a game over message, return `"RESET"` instead to make MLGame invoke `reset()`. 106 | * `reset(self)`: Do some reset stuffs for the next game round 107 | 108 | ### Non-python Client Support 109 | 110 | MLGame supports that a non-python script runs as a ml client. For the supported programming languages and how to use it, please view the [README](mlgame/crosslang/README.md) of the `mlgame.crosslang` module. 111 | 112 | ## Record Game Progress 113 | 114 | If `-r` flag is specified, the game progress will be recorded into a file, which is saved in `games//log/` directory. When a game round is ended, a file `_.pickle` is generated. The prefix of the filename contains the game mode and game parameters, such as `ml_EASY_2_2020-09-03_08-05-23.pickle`. These log files can be used to train the model. 115 | 116 | ### Format 117 | 118 | The dumped game progress is a dictionary. The first key is `"record_format_version"` which indicates the format version of the record file, and its value is 2 for the current mlgame version. The other keys are the name of ml clients which are defined by the game. Its value is also a dictionary which has two keys - `"scene_info"` and `"command"`. They sequentially stores the scene information and the command received or sent from that ml client. Note that the last element of `"command"` is always `None`, because there is no command to be sent when the game is over. 119 | 120 | The game progress will be like: 121 | 122 | ``` 123 | { 124 | "record_format_version": 2, 125 | "ml_1P": { 126 | "scene_info": [scene_info_0, scene_info_1, ... , scene_info_n-1, scene_info_n], 127 | "command": [command_0, command_1, ... , command_n-1, None] 128 | }, 129 | "ml_2P": { 130 | "scene_info": [scene_info_0, scene_info_1, ... , scene_info_n-1, scene_info_n], 131 | "command": [command_0, command_1, ... , command_n-1, None] 132 | }, 133 | "ml_3P": { 134 | "scene_info": [], 135 | "command": [] 136 | } 137 | } 138 | ``` 139 | 140 | If the scene information is not privided for the certain ml client, which the game runs with dynamic ml clients, it's value will be an empty list like "ml_3P" in the above example. 141 | 142 | ### Read Game Progress 143 | 144 | You can use `pickle.load()` to read the game progress from the file. 145 | 146 | Here is the example for read the game progress: 147 | 148 | ```python 149 | import pickle 150 | import random 151 | 152 | def print_log(): 153 | with open("path/to/log/file", "rb") as f: 154 | p = pickle.load(f) 155 | 156 | print("Record format version:", p["record_format_version"]) 157 | for ml_name in p.keys(): 158 | if ml_name == "record_format_version": 159 | continue 160 | 161 | target_record = p[ml_name] 162 | random_id = random.randrange(len(target_record["scene_info"])) 163 | print("Scene information:", target_record["scene_info"][random_id]) 164 | print("Command:", target_record["command"][random_id]) 165 | 166 | if __name__ == "__main__": 167 | print_log() 168 | ``` 169 | 170 | > For the non-python client, it may need to write a python script to read the record file and convert the game progess to other format (such as plain text) for the non-python client to read. 171 | 172 | ### Access Trained Data 173 | 174 | The ml script needs to load the trained data from external files. It is recommended that put these files in the same directory of the ml script and use absolute path to access them. 175 | 176 | For example, there are two files `ml_play.py` and `trained_data.sav` in the same ml directory: 177 | 178 | ```python 179 | from pathlib import Path 180 | import pickle 181 | 182 | class MLPlay: 183 | def __init__(self): 184 | # Get the absolute path of the directory in where this file is 185 | dir_path = Path(__file__).parent 186 | data_file_path = dir_path.joinpath("trained_data.sav") 187 | 188 | with open(data_file_path, "rb") as f: 189 | data = pickle.load(f) 190 | ``` 191 | 192 | ## Change Log 193 | 194 | View [CHANGELOG.md](./CHANGELOG.md) 195 | -------------------------------------------------------------------------------- /games/arkanoid/game/gameobject.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame import Rect, Surface 3 | from pygame.math import Vector2 4 | from pygame.sprite import Sprite 5 | import random 6 | 7 | from mlgame.gamedev import physics 8 | from mlgame.utils.enum import StringEnum, auto 9 | 10 | class Brick(Sprite): 11 | def __init__(self, init_pos, *groups): 12 | super().__init__(*groups) 13 | 14 | self.rect = Rect(init_pos[0], init_pos[1], 25, 10) 15 | self.image = self._create_surface((244, 158, 66)) # Orange 16 | 17 | def _create_surface(self, color): 18 | surface = Surface((self.rect.width, self.rect.height)) 19 | surface.fill(color) 20 | pygame.draw.line(surface, (0, 0, 0), 21 | (self.rect.width - 1, 0), (self.rect.width - 1, self.rect.height - 1)) 22 | pygame.draw.line(surface, (0, 0, 0), 23 | (0, self.rect.height - 1), (self.rect.width - 1, self.rect.height - 1)) 24 | return surface 25 | 26 | @property 27 | def pos(self): 28 | return self.rect.topleft 29 | 30 | class HardBrick(Brick): 31 | def __init__(self, init_pos, *groups): 32 | super().__init__(init_pos, *groups) 33 | 34 | self.reset() 35 | 36 | def reset(self): 37 | self.hp = 2 38 | # Override the origin color 39 | self.image = self._create_surface((209, 31, 31)) # Red 40 | 41 | def hit(self): 42 | """ 43 | Decrease 1 HP and change the color of image and return the remaining HP 44 | 45 | @return The remaining HP 46 | """ 47 | self.hp -= 1 48 | self.image = self._create_surface((244, 158, 66)) # Orange 49 | 50 | return self.hp 51 | 52 | class PlatformAction(StringEnum): 53 | SERVE_TO_LEFT = auto() 54 | SERVE_TO_RIGHT = auto() 55 | MOVE_LEFT = auto() 56 | MOVE_RIGHT = auto() 57 | NONE = auto() 58 | 59 | SERVE_BALL_ACTIONS = (PlatformAction.SERVE_TO_LEFT, PlatformAction.SERVE_TO_RIGHT) 60 | 61 | class Platform(Sprite): 62 | def __init__(self, init_pos, play_area_rect: Rect, *groups): 63 | super().__init__(*groups) 64 | 65 | self._play_area_rect = play_area_rect 66 | self._shift_speed = 5 67 | self._speed = [0, 0] 68 | self._init_pos = init_pos 69 | 70 | self.rect = Rect(init_pos[0], init_pos[1], 40, 5) 71 | self.image = self._create_surface() 72 | 73 | def _create_surface(self): 74 | surface = Surface((self.rect.width, self.rect.height)) 75 | surface.fill((66, 226, 126)) # Green 76 | return surface 77 | 78 | @property 79 | def pos(self): 80 | return self.rect.topleft 81 | 82 | def reset(self): 83 | self.rect.topleft = self._init_pos 84 | 85 | def move(self, move_action: PlatformAction): 86 | if (move_action == PlatformAction.MOVE_LEFT and 87 | self.rect.left > self._play_area_rect.left): 88 | self._speed[0] = -self._shift_speed 89 | elif (move_action == PlatformAction.MOVE_RIGHT and 90 | self.rect.right < self._play_area_rect.right): 91 | self._speed[0] = self._shift_speed 92 | else: 93 | self._speed[0] = 0 94 | 95 | self.rect.move_ip(*self._speed) 96 | 97 | class Ball(Sprite): 98 | def __init__(self, init_pos, play_area_rect: Rect, enable_slide_ball: bool, *groups): 99 | super().__init__(*groups) 100 | 101 | self._play_area_rect = play_area_rect 102 | self._do_slide_ball = enable_slide_ball 103 | self._init_pos = init_pos 104 | self._speed = [0, 0] 105 | 106 | self.hit_platform_times = 0 107 | 108 | self.rect = Rect(*self._init_pos, 5, 5) 109 | self.image = self._create_surface() 110 | 111 | # For additional collision checking 112 | self._last_pos = self.rect.copy() 113 | 114 | def _create_surface(self): 115 | surface = pygame.Surface((self.rect.width, self.rect.height)) 116 | surface.fill((44, 185, 214)) # Blue 117 | return surface 118 | 119 | @property 120 | def pos(self): 121 | return self.rect.topleft 122 | 123 | def reset(self): 124 | self.hit_platform_times = 0 125 | self.rect.topleft = self._init_pos 126 | self._speed = [0, 0] 127 | 128 | def stick_on_platform(self, platform_centerx): 129 | self.rect.centerx = platform_centerx 130 | 131 | def serve(self, platform_action: PlatformAction): 132 | if platform_action == PlatformAction.SERVE_TO_LEFT: 133 | self._speed = [-7, -7] 134 | elif platform_action == PlatformAction.SERVE_TO_RIGHT: 135 | self._speed = [7, -7] 136 | 137 | def move(self): 138 | self._last_pos.topleft = self.rect.topleft 139 | self.rect.move_ip(self._speed) 140 | 141 | def check_bouncing(self, platform: Platform): 142 | if (physics.collide_or_contact(self, platform) or 143 | self._platform_additional_check(platform)): 144 | self.hit_platform_times += 1 145 | 146 | rect_after_bounce, speed_after_bounce = physics.bounce_off( 147 | self.rect, self._speed, platform.rect, platform._speed) 148 | # Check slicing ball when the ball goes up after bouncing (not game over) 149 | if self._do_slide_ball and speed_after_bounce[1] < 0: 150 | speed_after_bounce[0] = self._slice_ball(self._speed[0], platform._speed[0]) 151 | 152 | self.rect = rect_after_bounce 153 | self._speed = speed_after_bounce 154 | 155 | if physics.rect_break_or_contact_box(self.rect, self._play_area_rect): 156 | physics.bounce_in_box_ip(self.rect, self._speed, self._play_area_rect) 157 | 158 | def _platform_additional_check(self, platform: Platform): 159 | """ 160 | The additional checking for the condition that the ball passes the corner of the platform 161 | """ 162 | if self.rect.bottom > platform.rect.top: 163 | routine_a = (Vector2(self._last_pos.bottomleft), Vector2(self.rect.bottomleft)) 164 | routine_b = (Vector2(self._last_pos.bottomright), Vector2(self.rect.bottomright)) 165 | 166 | return (physics.rect_collideline(platform.rect, routine_a) or 167 | physics.rect_collideline(platform.rect, routine_b)) 168 | 169 | return False 170 | 171 | def _slice_ball(self, ball_speed_x, platform_speed_x): 172 | """ 173 | Check if the platform slices the ball, and modify the ball speed. 174 | 175 | @return The new x speed of the ball after slicing 176 | """ 177 | # If the platform doesn't move, bounce normally. 178 | if platform_speed_x == 0: 179 | return 7 if ball_speed_x > 0 else -7 180 | # If the platform moves at the same direction as the ball moving, 181 | # speed up the ball. 182 | elif ball_speed_x * platform_speed_x > 0: 183 | return 10 if ball_speed_x > 0 else -10 184 | # If the platform moves at the opposite direction against the ball moving, 185 | # hit the ball back. 186 | else: 187 | return -7 if ball_speed_x > 0 else 7 188 | 189 | def check_hit_brick(self, group_brick: pygame.sprite.RenderPlain) -> int: 190 | """ 191 | Check if the ball hits bricks in the `group_brick`. 192 | The hit bricks will be removed from `group_brick`, but the alive hard brick will not. 193 | However, if the ball speed is high, the hard brick will be removed with only one hit. 194 | 195 | @param group_brick The sprite group containing bricks 196 | @return The number of destroyed bricks 197 | """ 198 | hit_bricks = pygame.sprite.spritecollide(self, group_brick, 1, 199 | physics.collide_or_contact) 200 | num_of_destroyed_brick = len(hit_bricks) 201 | 202 | if num_of_destroyed_brick > 0: 203 | # XXX: Bad multiple collision bouncing handling 204 | if (num_of_destroyed_brick == 2 and 205 | (hit_bricks[0].rect.y == hit_bricks[1].rect.y or 206 | hit_bricks[0].rect.x == hit_bricks[1].rect.x)): 207 | combined_rect = hit_bricks[0].rect.union(hit_bricks[1].rect) 208 | physics.bounce_off_ip(self.rect, self._speed, combined_rect, (0, 0)) 209 | else: 210 | physics.bounce_off_ip(self.rect, self._speed, hit_bricks[0].rect, (0, 0)) 211 | 212 | if abs(self._speed[0]) == 7: 213 | for brick in hit_bricks: 214 | if isinstance(brick, HardBrick) and brick.hit(): 215 | group_brick.add((brick,)) 216 | num_of_destroyed_brick -= 1 217 | 218 | return num_of_destroyed_brick 219 | -------------------------------------------------------------------------------- /games/pingpong/game/gameobject.py: -------------------------------------------------------------------------------- 1 | from mlgame.gamedev import physics 2 | from mlgame.utils.enum import StringEnum, auto 3 | 4 | from pygame.math import Vector2 5 | import pygame 6 | import random 7 | 8 | class PlatformAction(StringEnum): 9 | SERVE_TO_LEFT = auto() 10 | SERVE_TO_RIGHT = auto() 11 | MOVE_LEFT = auto() 12 | MOVE_RIGHT = auto() 13 | NONE = auto() 14 | 15 | SERVE_BALL_ACTIONS = (PlatformAction.SERVE_TO_LEFT, PlatformAction.SERVE_TO_RIGHT) 16 | 17 | class Platform(pygame.sprite.Sprite): 18 | def __init__(self, init_pos: tuple, play_area_rect: pygame.Rect, 19 | side, color, *groups): 20 | super().__init__(*groups) 21 | 22 | self._play_area_rect = play_area_rect 23 | self._shift_speed = 5 24 | self._speed = [0, 0] 25 | self._init_pos = init_pos 26 | 27 | self.rect = pygame.Rect(*init_pos, 40, 30) 28 | self.image = self._create_surface(side, color) 29 | 30 | def _create_surface(self, side, color): 31 | surface = pygame.Surface((self.rect.width, self.rect.height)) 32 | 33 | # Draw the platform image 34 | platform_image = pygame.Surface((self.rect.width, 10)) 35 | platform_image.fill(color) 36 | # The platform image of 1P is at the top of the rect 37 | if side == "1P": 38 | surface.blit(platform_image, (0, 0)) 39 | # The platform image of 2P is at the bottom of the rect 40 | else: 41 | surface.blit(platform_image, (0, surface.get_height() - 10)) 42 | 43 | # Draw the outline of the platform rect 44 | pygame.draw.rect(surface, color, 45 | pygame.Rect(0, 0, self.rect.width, self.rect.height), 1) 46 | 47 | return surface 48 | 49 | @property 50 | def pos(self): 51 | return self.rect.topleft 52 | 53 | def reset(self): 54 | self.rect.x, self.rect.y = self._init_pos 55 | 56 | def move(self, move_action: PlatformAction): 57 | if (move_action == PlatformAction.MOVE_LEFT and 58 | self.rect.left > self._play_area_rect.left): 59 | self._speed[0] = -self._shift_speed 60 | elif (move_action == PlatformAction.MOVE_RIGHT and 61 | self.rect.right < self._play_area_rect.right): 62 | self._speed[0] = self._shift_speed 63 | else: 64 | self._speed[0] = 0 65 | 66 | self.rect.move_ip(*self._speed) 67 | 68 | class Blocker(pygame.sprite.Sprite): 69 | def __init__(self, init_pos_y, play_area_rect: pygame.Rect, *groups): 70 | super().__init__(*groups) 71 | 72 | self._play_area_rect = play_area_rect 73 | self._speed = [random.choice((5 , -5)), 0] 74 | 75 | self.rect = pygame.Rect( 76 | random.randrange(0, play_area_rect.width - 10, 20), init_pos_y, 30, 20) 77 | self.image = self._create_surface() 78 | 79 | def _create_surface(self): 80 | surface = pygame.Surface((self.rect.width, self.rect.height)) 81 | surface.fill((213, 224, 0)) # Yellow-green 82 | return surface 83 | 84 | @property 85 | def pos(self): 86 | return self.rect.topleft 87 | 88 | def reset(self): 89 | self.rect.x = random.randrange(0, self._play_area_rect.width - 10, 20) 90 | self._speed = [random.choice((5, -5)), 0] 91 | 92 | def move(self): 93 | self.rect.move_ip(self._speed) 94 | 95 | if self.rect.left <= self._play_area_rect.left: 96 | self.rect.left = self._play_area_rect.left 97 | self._speed[0] *= -1 98 | elif self.rect.right >= self._play_area_rect.right: 99 | self.rect.right = self._play_area_rect.right 100 | self._speed[0] *= -1 101 | 102 | class Ball(pygame.sprite.Sprite): 103 | def __init__(self, play_area_rect: pygame.Rect, enable_slide_ball: bool, *groups): 104 | super().__init__(*groups) 105 | 106 | self._play_area_rect = play_area_rect 107 | self._speed = [0, 0] 108 | self._size = [5, 5] 109 | self._do_slide_ball = enable_slide_ball 110 | 111 | self.serve_from_1P = True 112 | 113 | self.rect = pygame.Rect(0, 0, *self._size) 114 | self.image = self._create_surface() 115 | 116 | # Used in additional collision detection 117 | self.last_pos = pygame.Rect(self.rect) 118 | 119 | def _create_surface(self): 120 | surface = pygame.Surface((self.rect.width, self.rect.height)) 121 | surface.fill((66, 226, 126)) # Green 122 | return surface 123 | 124 | @property 125 | def pos(self): 126 | return self.rect.topleft 127 | 128 | @property 129 | def speed(self): 130 | return tuple(self._speed) 131 | 132 | def reset(self): 133 | """ 134 | Reset the ball status 135 | """ 136 | self._speed = [0, 0] 137 | # Change side next time 138 | self.serve_from_1P = not self.serve_from_1P 139 | 140 | def stick_on_platform(self, platform_1P_rect, platform_2P_rect): 141 | """ 142 | Stick on the either platform according to the status of `_serve_from_1P` 143 | """ 144 | if self.serve_from_1P: 145 | self.rect.centerx = platform_1P_rect.centerx 146 | self.rect.y = platform_1P_rect.top - self.rect.height 147 | else: 148 | self.rect.centerx = platform_2P_rect.centerx 149 | self.rect.y = platform_2P_rect.bottom 150 | 151 | def serve(self, serve_ball_action: PlatformAction): 152 | """ 153 | Set the ball speed according to the action of ball serving 154 | """ 155 | self._speed[0] = { 156 | PlatformAction.SERVE_TO_LEFT: -7, 157 | PlatformAction.SERVE_TO_RIGHT: 7, 158 | }.get(serve_ball_action) 159 | 160 | self._speed[1] = -7 if self.serve_from_1P else 7 161 | 162 | def move(self): 163 | self.last_pos.topleft = self.rect.topleft 164 | self.rect.move_ip(self._speed) 165 | 166 | def speed_up(self): 167 | self._speed[0] += 1 if self._speed[0] > 0 else -1 168 | self._speed[1] += 1 if self._speed[1] > 0 else -1 169 | 170 | def check_bouncing(self, platform_1p: Platform, platform_2p: Platform, 171 | blocker: Blocker): 172 | # If the ball hits the play_area, adjust the position first 173 | # and preserve the speed after bouncing. 174 | hit_box = physics.rect_break_or_contact_box(self.rect, self._play_area_rect) 175 | if hit_box: 176 | self.rect, speed_after_hit_box = ( 177 | physics.bounce_in_box(self.rect, self._speed, self._play_area_rect)) 178 | 179 | # If the ball hits the specified sprites, adjust the position again 180 | # and preserve the speed after bouncing. 181 | hit_sprite = self._check_ball_hit_sprites((platform_1p, platform_2p, blocker)) 182 | if hit_sprite: 183 | self.rect, speed_after_bounce = physics.bounce_off( 184 | self.rect, self._speed, 185 | hit_sprite.rect, hit_sprite._speed) 186 | 187 | # Check slicing ball when the ball is caught by the platform 188 | if (self._do_slide_ball and 189 | ((hit_sprite is platform_1p and speed_after_bounce[1] < 0) or 190 | (hit_sprite is platform_2p and speed_after_bounce[1] > 0))): 191 | speed_after_bounce[0] = self._slice_ball(self._speed, hit_sprite._speed[0]) 192 | 193 | # Decide the final speed 194 | if hit_box: 195 | self._speed[0] = speed_after_hit_box[0] 196 | if hit_sprite: 197 | self._speed[1] = speed_after_bounce[1] 198 | if not hit_box: 199 | self._speed[0] = speed_after_bounce[0] 200 | 201 | def _check_ball_hit_sprites(self, sprites): 202 | """ 203 | Get the first sprite in the `sprites` that the ball hits 204 | 205 | @param sprites An iterable object that storing the target sprites 206 | @return The first sprite in the `sprites` that the ball hits. 207 | Return None, if none of them is hit by the ball. 208 | """ 209 | for sprite in sprites: 210 | if physics.moving_collide_or_contact(self, sprite): 211 | return sprite 212 | 213 | return None 214 | 215 | def _slice_ball(self, ball_speed, platform_speed_x): 216 | """ 217 | Check if the platform slices the ball, and modify the ball speed 218 | """ 219 | # The y speed won't be changed after ball slicing. 220 | # It's good for determining the x speed. 221 | origin_ball_speed = abs(ball_speed[1]) 222 | 223 | # If the platform moves at the same direction as the ball moving, 224 | # speed up the ball. 225 | if platform_speed_x * ball_speed[0] > 0: 226 | origin_ball_speed += 3 227 | # If they move to the different direction, 228 | # reverse the ball direction. 229 | elif platform_speed_x * ball_speed[0] < 0: 230 | origin_ball_speed *= -1 231 | 232 | return origin_ball_speed if ball_speed[0] > 0 else -origin_ball_speed 233 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The format is modified from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 4 | 5 | ### [Beta 8.0.1] - 2020.10.05 6 | 7 | **Changed** 8 | 9 | * The recorded game progress for the inactivated ml client will be an empty list. 10 | 11 | ### [Beta 8.0] - 2020.09.29 12 | 13 | **Added** 14 | 15 | * Add `"record_format_version"` field to the log file 16 | 17 | **Changed** 18 | 19 | * Change the data structure received from or sent to the game class: use a dict to store the information of each ml client. For example, the game has two players and defines the name of them as `"ml_1P"` and `"ml_2P"` in the `config.py`. 20 | * The scene information returned from `get_player_scene_info()` will be: 21 | ``` 22 | { 23 | "ml_1P": scene_info_for_ml_1P, 24 | "ml_2P": scene_info_for_ml_2P 25 | } 26 | ``` 27 | * The command sent to the `update()` will be: 28 | ``` 29 | { 30 | "ml_1P": command_sent_from_ml_1P, 31 | "ml_2P": command_sent_from_ml_2P 32 | } 33 | ``` 34 | * And the command returned from `get_keyboard_command()` should also be: 35 | ``` 36 | { 37 | "ml_1P": command_for_ml_1P, 38 | "ml_2P": command_for_ml_2P 39 | } 40 | ``` 41 | * Record the scene information and command individually for each ml client in the log file 42 | * Update the version of built-in games 43 | * arkanoid: 1.0 -> 1.1 44 | * pingpong: 1.1 -> 1.2 45 | * snake: 1.0 -> 1.1 46 | 47 | ### [Beta 7.2] - 2020.08.23 48 | 49 | **Fixed** 50 | 51 | * The game could not be closed when running non-python client 52 | 53 | **Changed** 54 | 55 | * `-i/--input-script` supports specifying the file in the subdirectory 56 | * For example, `-i foo/ml_play.py` means the file is at `games//ml/foo/ml_play.py` 57 | 58 | **Removed** 59 | 60 | * Remove `--input-module` flag 61 | 62 | ### [Beta 7.1.3] - 2020.06.20 63 | 64 | **Fixed** 65 | 66 | * Handle the exception of `SystemExit` 67 | * Handle the situation of reseting the ml executor before the game ends 68 | * The game executor will ignore the ready command. 69 | 70 | **Changed** 71 | 72 | * Optimize the checking of the received object from either side 73 | 74 | ### [Beta 7.1.2] - 2020.06.19 75 | 76 | **Fixed** 77 | 78 | * Use wrong value to chack frame delay 79 | 80 | **Changed** 81 | 82 | * Modify the game "snake" for the game development tutorial 83 | * The `ml_play_template.py` of the game "snake" contains simple rule-based algorithm. 84 | 85 | ### [Beta 7.1.1] - 2020.06.15 86 | 87 | **Changed** 88 | 89 | * ML process doesn't send the command only when the returned value of `MLPlay.update()` is `None`. 90 | 91 | ### [Beta 7.1] - 2020.06.01 92 | 93 | **Fixed** 94 | 95 | * Handle the exception of `BrokenPipeError` 96 | 97 | **Added** 98 | 99 | * Add "dynamic_ml_clients" to the "GAME_SETUP" of the game config 100 | 101 | ### [Beta 7.0.1] - 2020.05.29 102 | 103 | This update is compatible with Beta 7.0. 104 | 105 | **Fixed** 106 | 107 | * Hang when the game exits on Linux 108 | 109 | **Added** 110 | 111 | * Add `errno.py` to define the exit code of errors 112 | * Handle the exception occurred in manual mode 113 | 114 | **Changed** 115 | 116 | * Change the exit code of errors 117 | 118 | ### [Beta 7.0] - 2020.05.27 119 | 120 | **Added** 121 | 122 | * Use executors to control the execution loop 123 | * The game and the ml script only need to provide "class" for the executor to invoke (like an interface). 124 | * The game doesn't need to provide manual and ml version. Just one game class. 125 | * Replace `ml_loop()` with `MLPlay` class in ml script 126 | * Add commnuication manager 127 | * The manager for the ml process has a queue for storing the object received. If the queue has more than 15 objects, the oldest object will be dropped. 128 | 129 | **Changed** 130 | 131 | * Change the format of the recording game progress 132 | * Replace `PROCESSES` with `GAME_SETUP` in `config.py` of the game to setup the game and the ml scripts 133 | * Rename `GameConfig` to `ExecutionCommand` 134 | * Simplfy the `communication` package into a module 135 | 136 | **Removed** 137 | 138 | * Remove `record.py` and ml version of the game in the game directory 139 | 140 | ### [Beta 6.1] - 2020.05.06 141 | 142 | **Changed** 143 | 144 | * Pingpong - version 1.1 145 | * Shorten the ball speed increasing interval 146 | * Randomly set the initial position of the blocker, and speed up the moving speed of it 147 | 148 | ### [Beta 6.0] - 2020.04.28 149 | 150 | **Added** 151 | 152 | * Add `-l` and `--list` flag for listing available games 153 | * Use `config.py` in games to set up the game parameters and the game execution 154 | * Use `argparse` for generating and handling game parameters 155 | * List game parameters of a game by using `python MLGame.py -h` 156 | * Exit with non-zero value when an error occurred 157 | 158 | **Changed** 159 | 160 | * The game execution flags must be specified before the game name, including `-i/--input-script/--input-module` flags 161 | * `-i/--input-script/--input-module` flags carry one script or one module at a time. 162 | * Specify these flags multiple times for multiple scripts or modules, such as `-i script_1P -i script_2P`. 163 | * Games: Use dictionary objects as communication objects between game and ml processes for flexibility 164 | * The record file only contains dictionay objects and built-in types, therefore, it can be read outside the `mlgame` directory. 165 | * `mlgame.gamedev.recorder.RecorderHelper` only accepts dictionary object. 166 | * Code refactoring 167 | 168 | **Removed** 169 | 170 | * Games 171 | * Remove `main.py` (replaced by `config.py`) 172 | * Remove `communication.py` 173 | * For the ml script, use `mlgame.communication.ml` module to communicate with the game process. See `ml_play_template.py` for the example. 174 | * Remove `CommandReceiver` from the `mlgame.communication.game` 175 | * The game has to validate the command recevied by itself. 176 | 177 | ### [Beta 5.0.1] - 2020.03.06 178 | 179 | **Fixed** 180 | 181 | * Fix typo in the README of the arkanoid 182 | * Arkanoid: Add additional checking condition for the ball bouncing 183 | 184 | ### [Beta 5.0] - 2020.03.03 185 | 186 | **Added** 187 | 188 | * Arkanoid and Pingpong: 189 | * The serving position and direction of the ball can be decided 190 | * Add difficulties for different mechanisms 191 | * Add ball slicing mechanism 192 | * Arkanoid: Add hard bricks 193 | * Pingpong: Add blocker 194 | 195 | **Changed** 196 | 197 | * Update the python from 3.5 to 3.6: For the `auto()` of the custom `Enum` 198 | * Optimize the output of the error message 199 | * Refactor the game classes: Extract the drawing and recording functions 200 | * Add prefix to the filename of the record files 201 | * Physics: Optimize the ball bouncing algorithm 202 | 203 | ### [Beta 4.1] - 2019.11.06 204 | 205 | **Added** 206 | 207 | * New game - Snake 208 | * Add README to the game Arkanoid and Pingpong 209 | 210 | **Changed** 211 | 212 | * Update pygame from 1.9.4 to 1.9.6 213 | * Arkanoid and Pingpong (Follow the structure of the game Snake): 214 | * Move `SceneInfo` to the `gamecore.py` 215 | * Rename `GameInstruction` to `GameCommand` 216 | * Arkanoid: Add `command` member to `SceneInfo` 217 | * Trying to load the record files generated before beta 4.1 will get `AttributeError: 'SceneInfo' object has no attribute 'command'` error. 218 | * Code refactoring 219 | 220 | ### [Beta 4.0] - 2019.08.30 221 | 222 | **Added** 223 | 224 | * `mlgame` - MLGame development API 225 | * `--input-module` flag for specifying the absolute importing path of user modules 226 | 227 | **Changed** 228 | 229 | * Use 4 spaces instead of tab 230 | * Support one shot mode in the manual mode 231 | * Fit the existing games to the new API 232 | * Move the directory of game to "games" directory 233 | * Arkanoid: Wait for the ml process before start new round 234 | * Arkanoid: Change the communication API 235 | 236 | ### [Beta 3.2] - 2019.07.30 237 | 238 | **Changed** 239 | 240 | * Pingpong: Exchange the 1P and 2P side 241 | * Code refactoring 242 | 243 | ### [Beta 3.1] - 2019.05.28 244 | 245 | **Changed** 246 | 247 | * Pingpong: Set the height of the platform from 10 to 30 248 | * Optimize the collision detection algorithm 249 | 250 | ### [Beta 3.0] - 2019.05.22 251 | 252 | **Added** 253 | 254 | * 2P game "pingpong" 255 | 256 | **Changed** 257 | 258 | * Optimize the call stack message of ml process 259 | * Use `argparse` instead of `optparse` 260 | 261 | ### [Beta 2.2.2] - 2019.04.15 262 | 263 | **Fixed** 264 | 265 | * The game doesn't wait for the ready command 266 | 267 | ### [Beta 2.2.1] - 2019.04.12 268 | 269 | **Fixed** 270 | 271 | * The game hangs when the exception occurred before `ml_ready()` 272 | 273 | **Changed** 274 | 275 | * Some code refactoring and optimization 276 | 277 | ### [Beta 2.2] - 2019.04.01 278 | 279 | **Added** 280 | 281 | * `-i` and `--input-script` for specifying the custom ml script 282 | 283 | ### [Beta 2.1.1] - 2019.03.21 284 | 285 | **Added** 286 | 287 | * Print the whole call stack when the exception occurred 288 | 289 | ### [Beta 2.1] - 2019.03.18 290 | 291 | **Fixed** 292 | 293 | * Quit the game automatically when an exception occurred 294 | 295 | **Added** 296 | 297 | * `-1` and `--one-shot` for the one shot mode in ml mode 298 | * Version message 299 | 300 | ### [Beta 2.0] - 2019.02.27 301 | 302 | **Changed** 303 | 304 | * Use function call instead of class instance to invoke use code 305 | * Optimize the collision detection algorithm 306 | * Increase the difficulty of the game "arkanoid" 307 | 308 | **Added** 309 | 310 | * `-r` and `--record` to record the game progress 311 | -------------------------------------------------------------------------------- /mlgame/communication.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from queue import Queue 3 | 4 | from .exceptions import MLProcessError 5 | 6 | class CommunicationSet: 7 | """ 8 | A data class for storing a set of communication objects and 9 | providing an interface for accessing these objects 10 | 11 | Communication objects used for receiving objects must provide `recv()` and `poll()`, 12 | and them used for sending objects must provide `send()`. 13 | For example, the object of `multiprocessing.connection.Connection` is a valid 14 | communication object for both sending and receiving. 15 | 16 | @var _recv_end A dictionary storing communication objects which are used to 17 | receive objects 18 | @var _send_end A dictionary storing communication objects which are used to 19 | send objects 20 | """ 21 | def __init__(self): 22 | self._recv_end = {} 23 | self._send_end = {} 24 | 25 | def add_recv_end(self, name: str, comm_obj): 26 | """ 27 | Add a new communication object for receiving objects 28 | 29 | @param name The name for distinguishing the added object. It could be used to 30 | distinguish the communication target. 31 | @param comm_obj The communication object which has `recv()` and `poll()` functions 32 | """ 33 | if self._recv_end.get(name): 34 | raise ValueError("The name '{}' already exists in 'recv_end'".format(name)) 35 | 36 | if not hasattr(comm_obj, "recv") or not hasattr(comm_obj, "poll"): 37 | raise ValueError("'comm_obj' doesn't have 'recv' or 'poll' function") 38 | 39 | self._recv_end[name] = comm_obj 40 | 41 | def add_send_end(self, name: str, comm_obj): 42 | """ 43 | Add a new communication object for sending objects 44 | 45 | @param name The name for distinguishing the added object. It could be used to 46 | distinguish the communication target. 47 | @param comm_obj The communication object which has `send()` function 48 | """ 49 | if self._send_end.get(name): 50 | raise ValueError("The name '{}' already exists in 'send_end'".format(name)) 51 | 52 | if not hasattr(comm_obj, "send"): 53 | raise ValueError("'comm_obj' doesn't have 'send' function") 54 | 55 | self._send_end[name] = comm_obj 56 | 57 | def get_recv_end_names(self): 58 | """ 59 | Get the name of all registered communication objects used for receiving 60 | 61 | @return A dictionary view objects containing the key of `_recv_end` 62 | """ 63 | return self._recv_end.keys() 64 | 65 | def get_send_end_names(self): 66 | """ 67 | Get the name of all registered communication objects used for sending 68 | 69 | @return A dictionary view objects containing the key of `_send_end` 70 | """ 71 | return self._send_end.keys() 72 | 73 | def poll(self, name: str): 74 | """ 75 | Check whether the specified communication object has data to read 76 | 77 | @param name The name of the communication object 78 | """ 79 | return self._recv_end[name].poll() 80 | 81 | def recv(self, name: str, to_wait: bool = False): 82 | """ 83 | Receive object from the specified communication object 84 | 85 | @param name The name of the communication object 86 | @param to_wait Whether to wait until the object is arrived 87 | @return The received object. If `to_wait` is False and nothing available from 88 | the specified communication object, return None. 89 | """ 90 | if not to_wait and not self.poll(name): 91 | return None 92 | 93 | return self._recv_end[name].recv() 94 | 95 | def recv_all(self, to_wait: bool = False): 96 | """ 97 | Receive objects from all communication object registered for receiving 98 | 99 | If `to_wait` is True, it will wait for the object one by one. 100 | 101 | @param to_wait Whether to wait until the object is arrived 102 | @return A dictionary storing received objects. The key is the name of 103 | the communication object, the value is the received object. 104 | If `to_wait` is False and nothing to receive, the value will be None. 105 | """ 106 | objs = {} 107 | for comm_name in self._recv_end.keys(): 108 | objs[comm_name] = self.recv(comm_name, to_wait) 109 | 110 | return objs 111 | 112 | def send(self, obj, name: str): 113 | """ 114 | Send object via the specified communication object 115 | 116 | @param obj The object to be sent 117 | @param name The name of the communication object 118 | """ 119 | self._send_end[name].send(obj) 120 | 121 | def send_all(self, obj): 122 | """ 123 | Send object via all communication objects registered for sending 124 | 125 | @param obj The object to be sent 126 | """ 127 | for comm_obj in self._send_end.values(): 128 | comm_obj.send(obj) 129 | 130 | class CommunicationHandler: 131 | """ 132 | A data class for storing a sending and a receiving communication objects 133 | and providing interface for accessing them 134 | """ 135 | def __init__(self): 136 | self._recv_end = None 137 | self._send_end = None 138 | 139 | def set_recv_end(self, comm_obj): 140 | """ 141 | Set the communication object for receiving 142 | 143 | @param comm_obj The communication object which has `recv` and `poll` function 144 | """ 145 | if not hasattr(comm_obj, "recv") or not hasattr(comm_obj, "poll"): 146 | raise ValueError("'comm_obj' doesn't have 'recv' or 'poll' function") 147 | 148 | self._recv_end = comm_obj 149 | 150 | def set_send_end(self, comm_obj): 151 | """ 152 | Set the communication object for sending 153 | 154 | @param comm_obj The communication object which has `send` function 155 | """ 156 | if not hasattr(comm_obj, "send"): 157 | raise ValueError("'comm_obj' doesn't have 'send' function") 158 | 159 | self._send_end = comm_obj 160 | 161 | def poll(self): 162 | return self._recv_end.poll() 163 | 164 | def recv(self): 165 | return self._recv_end.recv() 166 | 167 | def send(self, obj): 168 | self._send_end.send(obj) 169 | 170 | class GameCommManager: 171 | """ 172 | The commnuication manager for the game process 173 | """ 174 | def __init__(self): 175 | self._comm_to_ml_set = CommunicationSet() 176 | 177 | def add_comm_to_ml(self, ml_name, recv_end, send_end): 178 | """ 179 | Set communication objects for communicating with specified ml process 180 | """ 181 | self._comm_to_ml_set.add_recv_end(ml_name, recv_end) 182 | self._comm_to_ml_set.add_send_end(ml_name, send_end) 183 | 184 | def get_ml_names(self): 185 | """ 186 | Get the name of all registered ml process 187 | """ 188 | return self._comm_to_ml_set.get_recv_end_names() 189 | 190 | def send_to_ml(self, obj, ml_name): 191 | """ 192 | Send the object to the specified ml process 193 | """ 194 | self._comm_to_ml_set.send(obj, ml_name) 195 | 196 | def send_to_all_ml(self, obj): 197 | """ 198 | Send the object to all ml process 199 | """ 200 | self._comm_to_ml_set.send_all(obj) 201 | 202 | def recv_from_ml(self, ml_name): 203 | """ 204 | Receive the object from the specified ml process 205 | 206 | If the received object is `MLProcessError`, raise the exception. 207 | """ 208 | obj = self._comm_to_ml_set.recv(ml_name, to_wait = False) 209 | if isinstance(obj, MLProcessError): 210 | raise obj 211 | return obj 212 | 213 | def recv_from_all_ml(self): 214 | """ 215 | Receive objects from all the ml processes 216 | """ 217 | obj_dict = {} 218 | for ml_name in self.get_ml_names(): 219 | obj_dict[ml_name] = self.recv_from_ml(ml_name) 220 | return obj_dict 221 | 222 | class MLCommManager: 223 | """ 224 | The communication manager for the ml process 225 | """ 226 | def __init__(self, ml_name): 227 | self._comm_to_game = CommunicationHandler() 228 | self._ml_name = ml_name 229 | 230 | def set_comm_to_game(self, recv_end, send_end): 231 | """ 232 | Set communication objects for communicating with game process 233 | 234 | @param recv_end The communication object for receiving objects from game process 235 | @param send_end The communication object for sending objects to game process 236 | """ 237 | self._comm_to_game.set_recv_end(recv_end) 238 | self._comm_to_game.set_send_end(send_end) 239 | 240 | def start_recv_obj_thread(self): 241 | """ 242 | Start a thread to keep receiving objects from the game 243 | """ 244 | self._obj_queue = Queue(15) 245 | 246 | thread = Thread(target = self._keep_recv_obj_from_game) 247 | thread.start() 248 | 249 | def _keep_recv_obj_from_game(self): 250 | """ 251 | Keep receiving object from the game and put it in the queue 252 | 253 | If the queue is full, the received object will be dropped. 254 | """ 255 | while True: 256 | if self._obj_queue.full(): 257 | self._obj_queue.get() 258 | print("Warning: The object queue for the process '{}' is full. " 259 | "Drop the oldest object." 260 | .format(self._ml_name)) 261 | 262 | obj = self._comm_to_game.recv() 263 | self._obj_queue.put(obj) 264 | if obj is None: # Received `None` from the game, quit the loop. 265 | break 266 | 267 | def recv_from_game(self): 268 | """ 269 | Receive an object from the game process 270 | 271 | @return The received object 272 | """ 273 | return self._obj_queue.get() 274 | 275 | def send_to_game(self, obj): 276 | """ 277 | Send an object to the game process 278 | 279 | @param obj An object to be sent 280 | """ 281 | try: 282 | self._comm_to_game.send(obj) 283 | except BrokenPipeError: 284 | print("Process '{}': The connection to the game process is closed." 285 | .format(self._ml_name)) 286 | -------------------------------------------------------------------------------- /mlgame/gamedev/physics.py: -------------------------------------------------------------------------------- 1 | """ 2 | The helper functions for physics 3 | """ 4 | 5 | from pygame import Rect 6 | from pygame.sprite import Sprite 7 | from pygame.math import Vector2 8 | 9 | def collide_or_contact(sprite_a: Sprite, sprite_b: Sprite) -> bool: 10 | """ 11 | Check if two sprites are colliding or contacting 12 | """ 13 | rect_a = sprite_a.rect 14 | rect_b = sprite_b.rect 15 | 16 | if (rect_a.left <= rect_b.right and 17 | rect_a.right >= rect_b.left and 18 | rect_a.top <= rect_b.bottom and 19 | rect_a.bottom >= rect_b.top): 20 | return True 21 | return False 22 | 23 | def moving_collide_or_contact(moving_sprite: Sprite, sprite: Sprite) -> bool: 24 | """ 25 | Check if the moving sprite collides or contacts another sprite. 26 | 27 | @param moving_sprite The sprite that moves in the scene. 28 | It must contain `rect` and `last_pos` attributes, which both are `pygame.Rect`. 29 | @param sprite The sprite that will be collided or contacted by `moving_sprite`. 30 | It must contain `rect` attribute, which is also `pygame.Rect`. 31 | """ 32 | # Generate the routine of 4 corners of the moving sprite 33 | move_rect = moving_sprite.rect 34 | move_last_pos = moving_sprite.last_pos 35 | routines = ( 36 | (Vector2(move_last_pos.topleft), Vector2(move_rect.topleft)), 37 | (Vector2(move_last_pos.topright), Vector2(move_rect.topright)), 38 | (Vector2(move_last_pos.bottomleft), Vector2(move_rect.bottomleft)), 39 | (Vector2(move_last_pos.bottomright), Vector2(move_rect.bottomright)) 40 | ) 41 | 42 | # Check any of routines collides the rect 43 | ## Take the bottom and right into account when using the API of pygame 44 | rect_expanded = sprite.rect.inflate(1, 1) 45 | for routine in routines: 46 | # Exclude the case that the `moving_sprite` goes from the surface of `sprite` 47 | if (not rect_expanded.collidepoint(routine[0]) and 48 | rect_collideline(sprite.rect, routine)): 49 | return True 50 | 51 | return False 52 | 53 | def line_intersect(line_a, line_b) -> bool: 54 | """ 55 | Check if two line segments intersect 56 | 57 | @param line_a A tuple (Vector2, Vector2) representing both end points 58 | of line segment 59 | @param line_b Same as `line_a` 60 | """ 61 | # line_a and line_b have the same end point 62 | if (line_a[0] == line_b[0] or 63 | line_a[1] == line_b[0] or 64 | line_a[0] == line_b[1] or 65 | line_a[1] == line_b[1]): 66 | return True 67 | 68 | # Set line_a to (u0, u0 + v0) and p0 = u0 + s * v0, and 69 | # set line_b to (u1, u1 + v1) and p1 = u1 + t * v1, 70 | # where u, v, p are vectors and s, t is in [0, 1]. 71 | # If line_a and line_b intersects, then p0 = p1 72 | # -> u0 - u1 = -s * v0 + t * v1 73 | # -> | u0.x - u1.x | | v0.x v1.x | |-s | 74 | # | u0.y - u1.y | = | v0.y v1.y | | t | 75 | # 76 | # If left-hand vector is a zero vector, then two line segments has the same end point. 77 | # If the right-hand matrix is not invertible, then two line segments are parallel. 78 | # If none of above conditions is matched, find the solution of s and t, 79 | # if both s and t are in [0, 1], then two line segments intersect. 80 | 81 | v0 = line_a[1] - line_a[0] 82 | v1 = line_b[1] - line_b[0] 83 | det = v0.x * v1.y - v0.y * v1.x 84 | # Two line segments are parallel 85 | if det == 0: 86 | # TODO Determine if two lines overlap 87 | return False 88 | 89 | du = line_a[0] - line_b[0] 90 | s_det = v1.x * du.y - v1.y * du.x 91 | t_det = v0.x * du.y - v0.y * du.x 92 | 93 | if ((det > 0 and 0 <= s_det <= det and 0 <= t_det <= det) or 94 | (det < 0 and det <= s_det <= 0 and det <= t_det <= 0)): 95 | return True 96 | 97 | return False 98 | 99 | def rect_collideline(rect: Rect, line) -> bool: 100 | """ 101 | Check if line segment intersects with a rect 102 | 103 | @param rect The Rect of the target rectangle 104 | @param line A tuple (Vector2, Vector2) representing both end points 105 | of line segment 106 | """ 107 | # Either of line ends is in the target rect. 108 | rect_expanded = rect.inflate(1, 1) # Take the bottom and right line into account 109 | if rect_expanded.collidepoint(line[0]) or rect_expanded.collidepoint(line[1]): 110 | return True 111 | 112 | line_top = (Vector2(rect.topleft), Vector2(rect.topright)) 113 | line_bottom = (Vector2(rect.bottomleft), Vector2(rect.bottomright)) 114 | line_left = (Vector2(rect.topleft), Vector2(rect.bottomleft)) 115 | line_right = (Vector2(rect.topright), Vector2(rect.bottomright)) 116 | 117 | return (line_intersect(line_top, line) or 118 | line_intersect(line_bottom, line) or 119 | line_intersect(line_left, line) or 120 | line_intersect(line_right, line)) 121 | 122 | def rect_break_or_contact_box(rect: Rect, box: Rect): 123 | """ 124 | Determine if the `rect` breaks the `box` or it contacts the border of `box` 125 | 126 | @param rect The Rect of the target rectangle 127 | @param box The target box 128 | """ 129 | return ( 130 | rect.left <= box.left or 131 | rect.right >= box.right or 132 | rect.top <= box.top or 133 | rect.bottom >= box.bottom) 134 | 135 | def bounce_off_ip(bounce_obj_rect: Rect, bounce_obj_speed, 136 | hit_obj_rect: Rect, hit_obj_speed): 137 | """ 138 | Calculate the speed and position of the `bounce_obj` after it bounces off the `hit_obj`. 139 | The position of `bounce_obj_rect` and the value of `bounce_obj_speed` will be updated. 140 | 141 | This function should be called only when two objects are colliding. 142 | 143 | @param bounce_obj_rect The Rect of the bouncing object 144 | @param bounce_obj_speed The 2D speed vector of the bouncing object. 145 | @param hit_obj_rect The Rect of the hit object 146 | @param hit_obj_speed The 2D speed vector of the hit object 147 | """ 148 | # Treat the hit object as an unmovable object 149 | speed_diff_x = bounce_obj_speed[0] - hit_obj_speed[0] 150 | speed_diff_y = bounce_obj_speed[1] - hit_obj_speed[1] 151 | 152 | # The relative position between top and bottom, and left and right 153 | # of two objects at the last frame 154 | rect_diff_bT_hB = hit_obj_rect.bottom - bounce_obj_rect.top + speed_diff_y 155 | rect_diff_bB_hT = hit_obj_rect.top - bounce_obj_rect.bottom + speed_diff_y 156 | rect_diff_bL_hR = hit_obj_rect.right - bounce_obj_rect.left + speed_diff_x 157 | rect_diff_bR_hL = hit_obj_rect.left - bounce_obj_rect.right + speed_diff_x 158 | 159 | # Get the surface distance from the bouncing object to the hit object 160 | # and the new position for the bouncing object if it really hit the object 161 | # according to their relative position 162 | ## The bouncing object is at the bottom 163 | if rect_diff_bT_hB < 0 and rect_diff_bB_hT < 0: 164 | surface_diff_y = rect_diff_bT_hB 165 | extract_pos_y = hit_obj_rect.bottom 166 | ## The bouncing object is at the top 167 | elif rect_diff_bT_hB > 0 and rect_diff_bB_hT > 0: 168 | surface_diff_y = rect_diff_bB_hT 169 | extract_pos_y = hit_obj_rect.top - bounce_obj_rect.height 170 | else: 171 | surface_diff_y = -1 if speed_diff_y > 0 else 1 172 | 173 | ## The bouncing object is at the right 174 | if rect_diff_bL_hR < 0 and rect_diff_bR_hL < 0: 175 | surface_diff_x = rect_diff_bL_hR 176 | extract_pos_x = hit_obj_rect.right 177 | ## The bouncing object is at the left 178 | elif rect_diff_bL_hR > 0 and rect_diff_bR_hL > 0: 179 | surface_diff_x = rect_diff_bR_hL 180 | extract_pos_x = hit_obj_rect.left - bounce_obj_rect.width 181 | else: 182 | surface_diff_x = -1 if speed_diff_x > 0 else 1 183 | 184 | # Calculate the duration to hit the surface for x and y coordination. 185 | time_hit_y = surface_diff_y / speed_diff_y 186 | time_hit_x = surface_diff_x / speed_diff_x 187 | 188 | if time_hit_y >= 0 and time_hit_y >= time_hit_x: 189 | bounce_obj_speed[1] *= -1 190 | bounce_obj_rect.y = extract_pos_y 191 | 192 | if time_hit_x >= 0 and time_hit_y <= time_hit_x: 193 | bounce_obj_speed[0] *= -1 194 | bounce_obj_rect.x = extract_pos_x 195 | 196 | def bounce_off(bounce_obj_rect: Rect, bounce_obj_speed, 197 | hit_obj_rect: Rect, hit_obj_speed): 198 | """ 199 | The alternative version of `bounce_off_ip`. The function returns the result 200 | instead of updating the value of `bounce_obj_rect` and `bounce_obj_speed`. 201 | 202 | @return A tuple (`new_bounce_obj_rect`, `new_bounce_obj_speed`) 203 | """ 204 | new_bounce_obj_rect = bounce_obj_rect.copy() 205 | new_bounce_obj_speed = bounce_obj_speed.copy() 206 | 207 | bounce_off_ip(new_bounce_obj_rect, new_bounce_obj_speed, 208 | hit_obj_rect, hit_obj_speed) 209 | 210 | return new_bounce_obj_rect, new_bounce_obj_speed 211 | 212 | def bounce_in_box_ip(bounce_obj_rect: Rect, bounce_obj_speed, 213 | box_rect: Rect): 214 | """ 215 | Bounce the object if it hits the border of the box. 216 | The speed and the position of the `bounce_obj` will be updated. 217 | 218 | @param bounce_obj_rect The Rect of the bouncing object 219 | @param bounce_obj_speed The 2D speed vector of the bouncing object. 220 | """ 221 | if bounce_obj_rect.left <= box_rect.left: 222 | bounce_obj_rect.left = box_rect.left 223 | bounce_obj_speed[0] *= -1 224 | elif bounce_obj_rect.right >= box_rect.right: 225 | bounce_obj_rect.right = box_rect.right 226 | bounce_obj_speed[0] *= -1 227 | 228 | if bounce_obj_rect.top <= box_rect.top: 229 | bounce_obj_rect.top = box_rect.top 230 | bounce_obj_speed[1] *= -1 231 | elif bounce_obj_rect.bottom >= box_rect.bottom: 232 | bounce_obj_rect.bottom = box_rect.bottom 233 | bounce_obj_speed[1] *= -1 234 | 235 | def bounce_in_box(bounce_obj_rect: Rect, bounce_obj_speed, 236 | box_rect: Rect): 237 | """ 238 | The alternative version of `bounce_in_box_ip`. The function returns the result 239 | instead of updating the value of `bounce_obj_rect` and `bounce_obj_speed`. 240 | 241 | @return A tuple (new_bounce_obj_rect, new_bounce_obj_speed) 242 | """ 243 | new_bounce_obj_rect = bounce_obj_rect.copy() 244 | new_bounce_obj_speed = bounce_obj_speed.copy() 245 | 246 | bounce_in_box_ip(new_bounce_obj_rect, new_bounce_obj_speed, box_rect) 247 | 248 | return (new_bounce_obj_rect, new_bounce_obj_speed) 249 | -------------------------------------------------------------------------------- /mlgame/loops.py: -------------------------------------------------------------------------------- 1 | """ 2 | The loop executor for running games and ml client 3 | """ 4 | 5 | import importlib 6 | import time 7 | import traceback 8 | 9 | from .communication import GameCommManager, MLCommManager 10 | from .exceptions import GameProcessError, MLProcessError 11 | from .gamedev.generic import quit_or_esc 12 | from .recorder import get_recorder 13 | 14 | class GameManualModeExecutor: 15 | """ 16 | The loop executor for the game process running in manual mode 17 | """ 18 | def __init__(self, execution_cmd, game_cls, ml_names): 19 | self._execution_cmd = execution_cmd 20 | self._game_cls = game_cls 21 | self._ml_names = ml_names 22 | self._frame_interval = 1 / self._execution_cmd.fps 23 | self._recorder = get_recorder(execution_cmd, ml_names) 24 | 25 | def start(self): 26 | """ 27 | Start the loop for running the game 28 | """ 29 | try: 30 | self._loop() 31 | except Exception: 32 | raise GameProcessError("game", traceback.format_exc()) 33 | 34 | def _loop(self): 35 | """ 36 | The main loop for running the game 37 | """ 38 | game = self._game_cls(*self._execution_cmd.game_params) 39 | 40 | while not quit_or_esc(): 41 | scene_info_dict = game.get_player_scene_info() 42 | time.sleep(self._frame_interval) 43 | cmd_dict = game.get_keyboard_command() 44 | self._recorder.record(scene_info_dict, cmd_dict) 45 | 46 | result = game.update(cmd_dict) 47 | 48 | if result == "RESET" or result == "QUIT": 49 | scene_info_dict = game.get_player_scene_info() 50 | self._recorder.record(scene_info_dict, {}) 51 | self._recorder.flush_to_file() 52 | 53 | if self._execution_cmd.one_shot_mode or result == "QUIT": 54 | break 55 | 56 | game.reset() 57 | 58 | class GameMLModeExecutorProperty: 59 | """ 60 | The data class that helps build `GameMLModeExecutor` 61 | """ 62 | def __init__(self, proc_name, execution_cmd, game_cls, ml_names): 63 | """ 64 | Constructor 65 | 66 | @param proc_name The name of the process 67 | @param execution_cmd A `ExecutionCommand` object that contains execution config 68 | @param game_cls The class of the game to be executed 69 | @param ml_names The name of all ml clients 70 | """ 71 | self.proc_name = proc_name 72 | self.execution_cmd = execution_cmd 73 | self.game_cls = game_cls 74 | self.ml_names = ml_names 75 | self.comm_manager = GameCommManager() 76 | 77 | class GameMLModeExecutor: 78 | """ 79 | The loop executor for the game process running in ml mode 80 | """ 81 | def __init__(self, propty: GameMLModeExecutorProperty): 82 | self._proc_name = propty.proc_name 83 | self._execution_cmd = propty.execution_cmd 84 | self._game_cls = propty.game_cls 85 | self._ml_names = propty.ml_names 86 | self._comm_manager = propty.comm_manager 87 | 88 | # Get the active ml names from the created ml processes 89 | self._active_ml_names = self._comm_manager.get_ml_names() 90 | self._ml_execution_time = 1 / self._execution_cmd.fps 91 | self._ml_delayed_frames = {} 92 | for name in self._active_ml_names: 93 | self._ml_delayed_frames[name] = 0 94 | self._recorder = get_recorder(self._execution_cmd, self._ml_names) 95 | self._frame_count = 0 96 | 97 | def start(self): 98 | """ 99 | Start the loop for the game process 100 | """ 101 | try: 102 | self._loop() 103 | except MLProcessError: 104 | # This exception wil be raised when invoking `GameCommManager.recv_from_ml()` 105 | # and receive `MLProcessError` object from it 106 | raise 107 | except Exception: 108 | raise GameProcessError(self._proc_name, traceback.format_exc()) 109 | 110 | def _loop(self): 111 | """ 112 | The loop for sending scene information to the ml process, recevied the command 113 | sent from the ml process, and pass command to the game for execution. 114 | """ 115 | game = self._game_cls(*self._execution_cmd.game_params) 116 | 117 | self._wait_all_ml_ready() 118 | while not quit_or_esc(): 119 | scene_info_dict = game.get_player_scene_info() 120 | cmd_dict = self._make_ml_execute(scene_info_dict) 121 | self._recorder.record(scene_info_dict, cmd_dict) 122 | 123 | result = game.update(cmd_dict) 124 | self._frame_count += 1 125 | 126 | # Do reset stuff 127 | if result == "RESET" or result == "QUIT": 128 | scene_info_dict = game.get_player_scene_info() 129 | for ml_name in self._active_ml_names: 130 | self._comm_manager.send_to_ml(scene_info_dict[ml_name], ml_name) 131 | self._recorder.record(scene_info_dict, {}) 132 | self._recorder.flush_to_file() 133 | 134 | if self._execution_cmd.one_shot_mode or result == "QUIT": 135 | break 136 | 137 | game.reset() 138 | self._frame_count = 0 139 | for name in self._active_ml_names: 140 | self._ml_delayed_frames[name] = 0 141 | self._wait_all_ml_ready() 142 | 143 | def _wait_all_ml_ready(self): 144 | """ 145 | Wait until receiving "READY" commands from all ml processes 146 | """ 147 | # Wait the ready command one by one 148 | for ml_name in self._active_ml_names: 149 | while self._comm_manager.recv_from_ml(ml_name) != "READY": 150 | pass 151 | 152 | def _make_ml_execute(self, scene_info_dict) -> dict: 153 | """ 154 | Send the scene information to all ml processes and wait for commands 155 | 156 | @return A dict of the recevied command from the ml clients 157 | If the client didn't send the command, it will be `None`. 158 | """ 159 | try: 160 | for ml_name in self._active_ml_names: 161 | self._comm_manager.send_to_ml(scene_info_dict[ml_name], ml_name) 162 | except KeyError as e: 163 | raise KeyError( 164 | "The game doesn't provide scene information " 165 | f"for the client '{ml_name}'") 166 | 167 | time.sleep(self._ml_execution_time) 168 | response_dict = self._comm_manager.recv_from_all_ml() 169 | 170 | cmd_dict = {} 171 | for ml_name in self._active_ml_names: 172 | cmd_received = response_dict[ml_name] 173 | if isinstance(cmd_received, dict): 174 | self._check_delay(ml_name, cmd_received["frame"]) 175 | cmd_dict[ml_name] = cmd_received["command"] 176 | else: 177 | cmd_dict[ml_name] = None 178 | 179 | return cmd_dict 180 | 181 | def _check_delay(self, ml_name, cmd_frame): 182 | """ 183 | Check if the timestamp of the received command is delayed 184 | """ 185 | delayed_frame = self._frame_count - cmd_frame 186 | if delayed_frame > self._ml_delayed_frames[ml_name]: 187 | self._ml_delayed_frames[ml_name] = delayed_frame 188 | print("The client '{}' delayed {} frame(s)".format(ml_name, delayed_frame)) 189 | 190 | class MLExecutorProperty: 191 | """ 192 | The data class that helps build `MLExecutor` 193 | """ 194 | def __init__(self, name, target_module, init_args = (), init_kwargs = {}): 195 | """ 196 | Constructor 197 | 198 | @param target_module The full name of the module to be executed in the process. 199 | The module must have `ml_loop` function. 200 | @param name The name of the ml process 201 | @param init_args The positional arguments to be passed to the `MLPlay.__init__()` 202 | @param init_kwargs The keyword arguments to be passed to the `MLPlay.__init__()` 203 | """ 204 | self.name = name 205 | self.target_module = target_module 206 | self.init_args = init_args 207 | self.init_kwargs = init_kwargs 208 | self.comm_manager = MLCommManager(name) 209 | 210 | class MLExecutor: 211 | """ 212 | The loop executor for the machine learning process 213 | """ 214 | 215 | def __init__(self, propty: MLExecutorProperty): 216 | self._name = propty.name 217 | self._target_module = propty.target_module 218 | self._init_args = propty.init_args 219 | self._init_kwargs = propty.init_kwargs 220 | self._comm_manager = propty.comm_manager 221 | self._frame_count = 0 222 | 223 | def start(self): 224 | """ 225 | Start the loop for the machine learning process 226 | """ 227 | self._comm_manager.start_recv_obj_thread() 228 | 229 | try: 230 | self._loop() 231 | except Exception as e: 232 | exception = MLProcessError(self._name, traceback.format_exc()) 233 | self._comm_manager.send_to_game(exception) 234 | except SystemExit: # Catch the exception made by 'sys.exit()' 235 | exception = MLProcessError(self._name, 236 | "The process '{}' is exited by itself. {}" 237 | .format(self._name, traceback.format_exc())) 238 | self._comm_manager.send_to_game(exception) 239 | 240 | def _loop(self): 241 | """ 242 | The loop for receiving scene information from the game, make ml class execute, 243 | and send the command back to the game. 244 | """ 245 | ml_module = importlib.import_module(self._target_module, __package__) 246 | ml = ml_module.MLPlay(*self._init_args, **self._init_kwargs) 247 | 248 | self._ml_ready() 249 | while True: 250 | scene_info = self._comm_manager.recv_from_game() 251 | if scene_info is None: 252 | break 253 | command = ml.update(scene_info) 254 | 255 | if command == "RESET": 256 | ml.reset() 257 | self._frame_count = 0 258 | self._ml_ready() 259 | continue 260 | 261 | if command is not None: 262 | self._comm_manager.send_to_game({ 263 | "frame": self._frame_count, 264 | "command": command 265 | }) 266 | 267 | self._frame_count += 1 268 | 269 | # Stop the client of the crosslang module 270 | if self._target_module == "mlgame.crosslang.ml_play": 271 | ml.stop_client() 272 | 273 | def _ml_ready(self): 274 | """ 275 | Send a "READY" command to the game process 276 | """ 277 | self._comm_manager.send_to_game("READY") 278 | --------------------------------------------------------------------------------