nekobean / mahjong-cpp

Miscellaneous programs about Japanese Mahjong
MIT License
99 stars 16 forks source link

feature request: portable library usage #7

Closed Equim-chan closed 1 year ago

Equim-chan commented 1 year ago

Hi, I'm the developer of Mortal, an open source mahjong AI. I've been looking for a way to properly integrate mahjong-cpp into Mortal because, for a long time, Mortal, more specifically the underlying libriichi, lacked such a hand searching mechanism, potentially resulting in a performance bottleneck. Here's my thoughts about this:

1. Use over C FFI

I found suitable interfaces in src/mahjong/json_parser.cpp. create_draw_response and create_discard_response are ideal for my usage, and structs like RequestData are well-defined. However, bridging Rust and C++ is a bit challenging and requires extensive revisions, including rewriting C++ struct in C (no more use of std::vector<double>) and creating a Rust wrapper for the C interface.

2. Rewrite in Rust

Although this was actually my first thought, I checked src/mahjong/expectedvaluecalculator.cpp and found it difficult due to the complexity of it. Additionally, the code structure of it is so different from libriichi that some glue code will need to be added, making this option no less burdensome.

3. Use over HTTP (server.cpp)

This is the most portable option, but may be less reliable or efficient. It would definitely make deployment and maintenance of Mortal-based applications harder as it introduces another dependency process.

How do you think of these options and do you have other suggestions? Thank you for this great work.

nekobean commented 1 year ago

Thank you for your interest in this repository. I have heard about Mortal AI.

mahjong-cpp has an interface for input/output in JSON string (specification). It is used for the web application "何切るシミュレーター" (https://pystyle.info/apps/mahjong-nanikiru-simulator/)

I am not familiar with Rust. If the following function exists, can it be easily ported to Rust?

std::string calc_exp_value(std::string request);
Equim-chan commented 1 year ago

Yes that would be easier. If the strings are replaced with actual C structs that will be more efficient, but I think this can be done later as we can try it with JSON first.

Equim-chan commented 1 year ago

I tried to translate the structs in json_parser.hpp into C. Note that this is not tested yet.

#ifdef __cplusplus
extern "C" {
#endif

typedef struct MeldedBlock {
    uint8_t type; // enum?
    size_t tils_len;
    uint8_t *tiles;
    uint8_t discarded_tile;
    uint8_t from;
} MeldedBlock;

typedef struct RequestData {
    uint8_t zikaze;
    uint8_t bakaze;
    uint8_t turn;

    size_t dora_indicators_len;
    uint8_t *dora_indicators;

    size_t handle_tiles_len;
    uint8_t *handle_tiles;

    size_t melded_blocks_len;
    MeldedBlock *melded_blocks;

    uint8_t flag;
    uint8_t counts[37];
} RequestData;

typedef struct DrawResponseData {
    size_t time_us;

    size_t required_tiles_len;
    uint8_t (*required_tiles)[2];

    size_t tenpai_probs_len;
    double *tenpai_probs;

    size_t win_probs_len;
    double *win_probs;

    size_t exp_values_len;
    double *exp_values;
} DrawResponseData;

typedef struct Candidate {
    uint8_t tile;

    /*! 巡目ごとの聴牌確率 */
    size_t tenpai_probs_len;
    double *tenpai_probs;

    /*! 巡目ごとの和了確率 */
    size_t win_probs_len;
    double *win_probs;

    /*! 巡目ごとの期待値 */
    size_t exp_values_len;
    double *exp_values;

    /*! 有効牌及び枚数の一覧 */
    size_t required_tiles_len;
    uint8_t (*required_tiles)[2];

    /*! 向聴戻しになるかどうか */
    bool syanten_down;
} Candidate;

typedef struct DiscardResponseData {
    size_t time_us;
    size_t candidates_len;
    Candidate *candidates;
} DiscardResponseData;

#ifdef __cplusplus
}
#endif

I removed shanten-related fields because Mortal already has it.

The function signatures would be like:

extern "C" {
    DrawResponseData *calc_exp_value_draw(const RequestData *req);
    DiscardResponseData *calc_exp_value_discard(const RequestData *req);

    // These free functions are cascade; they must recursively call free to all their fields.
    void free_draw_response_data(DrawResponseData *ptr);
    void free_discard_response_data(DiscardResponseData *ptr);
}
nekobean commented 1 year ago

I will create an API that interacts in JSON or struct commented above. I hadn't updated the code in almost 2 years, so please give me a week to recall.

Equim-chan commented 1 year ago

I actually tried rewriting it in Rust and it worked like a charm, turns out it was not as difficult as I thought. I will release the Rust port of the algorithm as a part of the next release of Mortal.

Since my initial problem is resolved, I think this issue can now be closed. If anyone else needs it in the future we can reopen it.

nekobean commented 11 months ago

Sorry for the late reply. I haven't logged in to GitHub for a while. Glad to hear the problem was solved.