The non-python script for the ml client is also placed in the ml
directory of the game.
If the user specifies a non-python script, MLGame will:
- Invoke
mlgame.crosslang.main
module to handle the script. mlgame.crosslang.main
module will check the file extension of the script, and find corresponding programming languagelang
inEXTENSION_LANG_MAP
defined inext_lang.map.py
.- Invoke
compile_script()
defined in the modulemlgame.crosslang.compile.<lang>.main
to compile the script. For example, if the user specifiesml_play.cpp
, themlgame.crosslang.compile.cpp.main
module is used. - 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.
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++.
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.
- Create a directory in
mlgame/crosslang/compile
and name it to the language name - Create
__init__.py
andmain.py
in the new directory - Edit
main.py
with the following code:
from pathlib import Path
from mlgame.crosslang.exceptions import CompilationError
def compile_script(script_full_path):
# Code for handling the source code #
# Raise CompilationError if an error occurred
if error_occurred:
raise CompilationError(Path(script_full_path).name, reason)
# Generate the command segments stored in a list object #
return ["commnd", "path/to/file", "--foo", "--bar"]
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.
- Edit
mlgame/crosslang/ext_lang_map.py
and add the "file extension-language" pair (".js": "javascript" for example) to the dictionaryEXTENSION_LANG_MAP
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.
There are 5 functions to be implemented:
Get the object sent from the game
get_init_args()
: Get the initial arguments. It will return an usable object.- Received string format:
"__init__ {"args": [...], "kwargs": {...}}\n"
- Received string format:
get_scene_info()
: Get the scene information from the game. It will return an usable object.- Received string format:
"__scene_info__ <scene_info_in_JSON_str>\n"
.scene_info
is a dictionary object sent from the game.
- Received string format:
For example:
json get_scene_info()
{
string scene_info_str;
getline(cin, scene_info_str);
// Ignore header "__scene_info__"
scene_info_str.erase(0, scene_info_str.find_first_of('{'));
return json::parse(scene_info_str);
}
Send the object back to the game
client_ready()
: Send the ready command to the game.- Sent string format:
"__command__ READY\n"
- Sent string format:
client_reset()
: Send the reset command to the game.- Sent string format:
"__command__ RESET\n"
- Sent string format:
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.- Sent string format:
"__command__ <game_command_in_JSON_str>\n"
.
- Sent string format:
For example:
void client_reset()
{
cout << "__command__ RESET" << endl;
}
The message received from the client without "__command__"
header will be printed out directly.
>>> 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.
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
:
constructor(init_args)
: Do initialization jobs.init_args
is the initial arguments sent from the game.update(scene_info) -> [game|reset]command
: Handle the scene information and generate the command.- The returned command could be game command or the reset command (could be a
"RESET"
for example)
- The returned command could be game command or the reset command (could be a
reset()
: Do reset jobs.
The flow chart of the loop:
For example:
int main()
{
AbstractMLPlay *ml = new MLPlay(get_init_args());
client_ready();
json command;
while (1) {
command = ml->update(get_scene_info());
if (command == "RESET") {
client_reset();
ml->reset();
client_ready();
continue;
}
send_command(command);
}
delete ml;
return 0;
}