bug-gnubg
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Inquiry on Integrating GNU Backgammon for Analysis via RESTful API


From: David Reay
Subject: Re: Inquiry on Integrating GNU Backgammon for Analysis via RESTful API
Date: Mon, 1 Apr 2024 18:35:48 +0100

Dear Phillippe,

Thank you for your email, this was very helpful. I managed to get something working although it has been hacked together:

I make a subprocess call to the gnubg exe:

def call_gnubg(action: str = "session", match_ref: str = str(uuid4()), match_length: str = "1"):
    """
    Calls the GNU Backgammon (gnubg) program from Python using the subprocess module.
    Adjust the command and arguments according to your specific requirements.
    """
    # Create a copy of the current environment variables
    env = os.environ.copy()
    # Set the custom environment variable
    env["MATCH_REF"] = str(match_ref)
    env["MATCH_LENGTH"] = str(match_length)
    env["ACTION"] = str(action)
    env["HOME"] = "/tmp" # required as gnubg needs to create .gnubg to run

    # Command to run gnubg. Modify the command according to your use case.
    # For example, you might want to run a specific command or script within gnubg.
    command = ["/usr/games/gnubg", "-q", "-t", "-p", "libgnubg.py"]
    # If you have specific arguments or need to interact with gnubg,
    # add them to the command list. For example: ["gnubg", "--some-argument", "value"]

    try:
        # Run the command and wait for it to complete
        result = subprocess.run(
            command,
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            env=env,
        )

        # Output the result (stdout)
        print("GNU Backgammon output: %s", result.stdout)

        # If needed, you can also handle errors using result.stderr
        if result.stderr:
            print("GNU Backgammon error output: %s", result.stderr)
    except subprocess.CalledProcessError as e:
        print("Error running GNU Backgammon: %s", e)
        raise
    except Exception as e:
        print("An unexpected error occurred: %s", e)
        raise



Which then calls a libgnubg from gnubg:

import base64
import gzip
import os
import json
import gnubg


def compress_data(data):
    """Compress data using gzip and encode it with base64."""
    compressed_data = gzip.compress(data.encode("utf-8"))
    return base64.b64encode(compressed_data).decode("utf-8")


def decompress_data(data):
    """Decode from base64 and decompress data using gzip."""
    decoded_data = base64.b64decode(data)
    decompressed_data = gzip.decompress(decoded_data)
    return decompressed_data.decode("utf-8")


def read_game_content_from_file(file_path):
    """Read game content from a local file."""
    with open(file_path, "r", encoding="utf-8") as file:
        return file.read()


def delete_local_file(file_path):
    """Delete the specified file."""
    try:
        os.remove(file_path)
        # print(f"File '{file_path}' was successfully deleted.")
    except OSError as e:
        print(f"Error: {e.strerror} - {e.filename}")


def write_dict_to_json_file(data_dict, match_ref):
    """
    Writes the provided dictionary to a JSON file named after the match reference.

    Parameters:
    - data_dict (dict): The dictionary to be converted into a JSON string.
    - match_ref (str): The reference name for the match, which is used as the filename.

    The function writes the JSON representation of `data_dict` to a file named `{match_ref}.json`.
    """
    # Define the filename based on match_ref
    filename = f"/tmp/{match_ref}/{match_ref}.json"

    # Write the dictionary to a JSON file
    with open(filename, "w") as json_file:
        json.dump(data_dict, json_file, indent=4)


def new_gnubg_match(match_length: str, match_ref: str) -> str:
    """
    Creates a new GNU Backgammon match and saves it to a specified directory.

    Parameters:
    - match_length (int): The length of the backgammon match.
    - match_ref (str): The reference name for the match, which is used to name the directory and match file.

    Returns:
    - None: The function prints the match details.
    """
    # Create the directory for the match
    match_dir = f"/tmp/{match_ref}"
    os.makedirs(match_dir, exist_ok=True)

    # Create a new match and save it
    gnubg.command(f"new match {match_length}")
    gnubg.command(f"save match {match_dir}/{match_ref}.sgf")


def new_gnubg_session(match_ref: str) -> str:
    """
    Creates a new GNU Backgammon match and saves it to a specified directory.

    Parameters:
    - match_length (int): The length of the backgammon match.
    - match_ref (str): The reference name for the match, which is used to name the directory and match file.

    Returns:
    - None: The function prints the match details.
    """
    # Create the directory for the match
    match_dir = f"/tmp/{match_ref}"
    os.makedirs(match_dir, exist_ok=True)

    # Create a new match and save it
    gnubg.command(f"new session")
    gnubg.command(f"save match {match_dir}/{match_ref}.sgf")


def load_match(match_ref: str):
    gnubg.command(f"load match {match_ref}.sgf")


#  print(dir(gnubg))
"""
        data_dict = {
            "sgf": compressed_data,
            # board
            # "board": gnubg.board(),
            # calcgammonprice
            #  "gammonprice": gnubg.calcgammonprice(),
            # cfevaluate
            # "cfevaluate": gnubg.cfevaluate(),
            # classifypos
            # command
            # cubeinfo
            # dicerolls
            # eq2mwc
            # eq2mwc_stderr
            # errorrating
            # evalcontext
            # evaluate
            # findbestmove
            # getevalhintfilter
            # gnubgid
            "gnubgid": gnubg.gnubgid(),
            # hint
            # luckrating
            # match
            "match": gnubg.match(),
            # matchchecksum
            "matchchecksum": gnubg.matchchecksum(),
            # matchid
            "matchid": gnubg.matchid(),
            # met
            # "met": gnubg.met(),
            # movetupletostring
            # mwc2eq
            # "mwc2eq": gnubg.mwc2eq(),
            # mwc2eq_stderr
            # "mwc2eq_stderr": gnubg.mwc2eq_stderr(),
            # navigate
            # nextturn
            # parsemove
            # posinfo
            # "posinfo": gnubg.posinfo(),
            # positionbearoff
            # positionfrombearoff
            # positionfromid
            # positionfromkey
            # positionid
            "positionid": gnubg.positionid(),
            # positionkey
            # rolloutcontext
            # setevalhintfilter
            # updateui
"""

if __name__ == "__main__":

    match_ref = os.environ["MATCH_REF"]
    match_length = os.environ["MATCH_LENGTH"]
    action = "">
    if action == "new_match":
        new_gnubg_match(match_ref, match_length)

    if action == "session":
        new_gnubg_session(match_ref)

    # Create game data for a log of the game
    game_data = read_game_content_from_file(f"/tmp/{match_ref}/{match_ref}.sgf")
    compressed_data = compress_data(game_data)

    # Combine into dictionary
    data_dict = {
        "sgf": compressed_data,
        "gnubgid": gnubg.gnubgid(),
        "match": gnubg.match(),
        "matchchecksum": gnubg.matchchecksum(),
        "matchid": gnubg.matchid(),
        "positionid": gnubg.positionid(),
    }

    # Write to file system
    write_dict_to_json_file(data_dict, match_ref)


I have to write to the data to the file system, and use environment variables to pass in variables to the subprocess, but it seems to work.

Regards,


David.


On Mon, 1 Apr 2024 at 13:25, Philippe Michel <philippe.michel7@free.fr> wrote:
On Wed, Mar 27, 2024 at 03:00:06PM +0000, David Reay wrote:

> I hope this message finds you well. I am currently developing a RESTful API
> using Python, specifically with frameworks such as FastAPI/Flask, and I am
> keen on integrating GNU Backgammon (gnubg) into this project. The primary
> function I aim to achieve is to send a match and position ID to gnubg, have
> it load the match, analyze the position, and if it is gnubg's turn, to also
> receive the recommended move.
>
> The end goal is to facilitate this interaction through JSON for both the
> requests and responses, ensuring seamless integration within a Python
> environment. This setup is intended to serve a broader application that
> leverages gnubg's capabilities for analysis and decision-making in
> backgammon matches.
>
> Could you please provide guidance or point me towards documentation on how
> to achieve the following using gnubg?
>
> 1. Sending a match and position ID to gnubg, preferably in a JSON format,
> for it to load and analyze the match.
> 2. Receiving gnubg's analysis and recommended move in a JSON response,
> which is crucial for the integration with my Python-based API.
> 3. Any examples or best practices for integrating gnubg with Python,
> especially in a serverless architecture or a containerized environment.

I'm not familiar with cient-server programming and json encoding in
python, but gnubg includes an embedded python interpreter, so anything
that works with regular python should (with Linux where the system
python libraries are used) or could (with Windows where a limited set of
modules are included in gnubg's installer and you may need to add some
missing ones).

For instance the following short examples (picked from searches, I can't
explain how they work exactly or how much oversimplified they are but
someone more familiar with what you want to do should be able to) can be
launched from gnubg. Or more probably the server part from gnubg and the
client part from regular python.

server.py:

import socketserver
import json

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        self.data = "">         print("{} wrote:".format(self.client_address[0]))
        print(self.data)

        j = json.loads(self.data)

        self.wfile.write(str(j).format().encode('utf-8'))

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        server.serve_forever()


client.py:

import socket
import sys
import json

HOST, PORT = "localhost", 9999
data = "">
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))

    received = str(sock.recv(1024), "utf-8")

print("Sent:     {}".format(data))
print("Received: {}".format(received))


To do something useful, the server part's handler can use the following
functions:

gnubg.setgnubgid() to set the position
and then either
gnubg.findbestmove() to find the best checker play
or
gnubg.cfevaluate() to evaluate a cube decision


>>> gnubg.setgnubgid("cC/kAVDatg0iAA:QYkVAAAAAAAA")
Setting GNUbg ID cC/kAVDatg0iAA:QYkVAAAAAAAA
>>> gnubg.findbestmove()
(8, 5, 7, 2)
>>> mt = gnubg.findbestmove()
>>> gnubg.movetupletostring(mt, gnubg.board())
'8/5 7/2'

(the latter needs gnubg.board() to distinguish 8/5 from 8/5* for instance...)

or

>>> gnubg.setgnubgid("4HPwA0AzTvABMA:cAkAAAAAAAAA")
Setting GNUbg ID 4HPwA0AzTvABMA:cAkAAAAAAAAA
>>> gnubg.cfevaluate()
(0.5438268780708313, 0.5438268780708313, 0.5045644044876099, 1.0, 2, 'No double, take')

The result tuple is:
estimated cubeful equity after the best decision
ND equity
D/T equity
D/P equity
index of the best decision (from the cubedecision enum in eval.h)
description of the best decision

reply via email to

[Prev in Thread] Current Thread [Next in Thread]