diff options
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | _chess_h_test.c | 1 | ||||
| -rw-r--r-- | include/hybbx/chess.h | 106 | ||||
| -rw-r--r-- | include/hybbx/plugin.h | 20 | ||||
| -rw-r--r-- | include/hybbx/service.h | 10 | ||||
| -rw-r--r-- | plugins/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | plugins/entertain/CMakeLists.txt | 27 | ||||
| -rw-r--r-- | plugins/entertain/chess.c | 552 | ||||
| -rw-r--r-- | plugins/entertain/chess_cmd.c | 277 | ||||
| -rw-r--r-- | plugins/entertain/entertain.c | 67 | ||||
| -rw-r--r-- | plugins/entertain/entertain_chess.h | 88 | ||||
| -rw-r--r-- | share/commands.yaml | 4 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/core/chess.c | 668 | ||||
| -rw-r--r-- | src/core/chess_cmd.c | 397 | ||||
| -rw-r--r-- | src/core/command.c | 9 | ||||
| -rw-r--r-- | src/core/service.c | 42 | ||||
| -rw-r--r-- | src/main.c | 6 | ||||
| -rw-r--r-- | text/news.txt | 2 | ||||
| -rw-r--r-- | text/rules.txt | 2 |
20 files changed, 1101 insertions, 1185 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a6c32..c715036 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ option(HYBBX_PLUGIN_SSH "Build SSH transport (requires libssh)" ON) option(HYBBX_PLUGIN_BAYCOM "Build BayCom/based transport plugin (default ON)" ON) option(HYBBX_PLUGIN_MAINS_PROXY "Build Main-to-Main mesh proxy" ON) option(HYBBX_PLUGIN_WEBSOCKET "Build WebSocket forward-proxy transport" ON) +option(HYBBX_PLUGIN_ENTERTAIN "Build Entertain plugin (chess)" ON) option(HYBBX_BUILD_TESTS "Build unit tests" OFF) option(HYBBX_HARDENING "Enable compiler security hardening" ON) option(HYBBX_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) @@ -181,6 +182,7 @@ if(HYBBX_PLATFORM_AMIGAOS) set(HYBBX_PLUGIN_MAINS_PROXY OFF CACHE BOOL "AmigaOS build" FORCE) set(HYBBX_PLUGIN_ARDOP OFF CACHE BOOL "AmigaOS build" FORCE) set(HYBBX_PLUGIN_CRDOP OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_ENTERTAIN OFF CACHE BOOL "AmigaOS build" FORCE) message(STATUS "HyBBX: AmigaOS — hybbx-telnet client (clib2); telnet + packet_radio plugins") endif() diff --git a/_chess_h_test.c b/_chess_h_test.c new file mode 100644 index 0000000..2ad32f7 --- /dev/null +++ b/_chess_h_test.c @@ -0,0 +1 @@ +#include "hybbx/chess.h" diff --git a/include/hybbx/chess.h b/include/hybbx/chess.h deleted file mode 100644 index bdfaa7b..0000000 --- a/include/hybbx/chess.h +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef HYBBX_CHESS_H -#define HYBBX_CHESS_H - -#include "hybbx/types.h" -#include "hybbx/limits.h" -#include "hybbx/storage.h" -#include "hybbx/service.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define CHESS_BOARD_SIZE 8 -#define CHESS_SQ_MAX 4 -#define CHESS_MOVE_MAX 8 -#define CHESS_SAN_MAX 8 -#define CHESS_GAME_MAX 16 /* array upper bound */ -#define CHESS_SPECTATOR_MAX 32 -#define CHESS_REFRESH_SEC 15 -#define CHESS_DEFAULT_MAX 8 /* default active games if INI omits [chess] */ - -#define CHESS_EMPTY '.' - -typedef enum { - CHESS_COLOR_NONE = 0, - CHESS_WHITE, - CHESS_BLACK -} chess_color_t; - -typedef enum { - CHESS_RESULT_NONE = 0, - CHESS_WHITE_WINS, - CHESS_BLACK_WINS, - CHESS_DRAW_STALEMATE, - CHESS_DRAW_AGREEMENT -} chess_result_t; - -typedef struct chess_board { - char sq[CHESS_BOARD_SIZE][CHESS_BOARD_SIZE]; -} chess_board_t; - -typedef struct chess_game { - int active; - char white_name[HYBBX_USER_NAME_MAX]; - char black_name[HYBBX_USER_NAME_MAX]; - struct hybbx_session *white_session; - struct hybbx_session *black_session; - struct hybbx_session *spectators[CHESS_SPECTATOR_MAX]; - unsigned spectator_count; - chess_board_t board; - chess_color_t turn; - unsigned move_number; - chess_result_t result; - char last_move_san[CHESS_SAN_MAX]; - unsigned tick_counter; - char game_id[8]; -} chess_game_t; - -void hybbx_chess_init(void); -void hybbx_chess_shutdown(void); -void hybbx_chess_config_apply(const struct hybbx_config *config); -unsigned hybbx_chess_max_games(void); -void hybbx_chess_shutdown(void); - -chess_game_t *hybbx_chess_create_game(struct hybbx_session *white, - const char *white_name); -chess_game_t *hybbx_chess_find_open_game(void); -chess_game_t *hybbx_chess_find_game_by_id(const char *id); -chess_game_t *hybbx_chess_get_game(unsigned index); -int hybbx_chess_join_game(chess_game_t *game, - struct hybbx_session *black, - const char *black_name); -int hybbx_chess_add_spectator(chess_game_t *game, - struct hybbx_session *spec); -void hybbx_chess_remove_spectator(chess_game_t *game, - struct hybbx_session *spec); -int hybbx_chess_is_player(const chess_game_t *game, - const struct hybbx_session *session); -int hybbx_chess_is_spectator(const chess_game_t *game, - const struct hybbx_session *session); -chess_color_t hybbx_chess_player_color(const chess_game_t *game, - const struct hybbx_session *session); -int hybbx_chess_make_move(chess_game_t *game, - const char *from, const char *to, - char promote, char *err, size_t errlen); -void hybbx_chess_render_board(const chess_board_t *board, - struct hybbx_session *session, - const char *label); -void hybbx_chess_send_board(chess_game_t *game, - struct hybbx_session *session); -void hybbx_chess_tick(struct hybbx_service *service); -void hybbx_chess_resign(chess_game_t *game, - struct hybbx_session *who); -void hybbx_chess_offer_draw(chess_game_t *game, - struct hybbx_session *who); - -/* Command handler - called from command.c dispatch */ -hybbx_result_t cmd_chess(struct hybbx_service *service, - struct hybbx_session *session, - const struct hybbx_parsed_command *cmd); - -#ifdef __cplusplus -} -#endif - -#endif /* HYBBX_CHESS_H */ diff --git a/include/hybbx/plugin.h b/include/hybbx/plugin.h index ea72a65..9a51211 100644 --- a/include/hybbx/plugin.h +++ b/include/hybbx/plugin.h @@ -9,6 +9,7 @@ extern "C" { struct hybbx_service; struct hybbx_session; +struct hybbx_parsed_command; /** * Transport plugin interface (link adapters / host-client bridges). @@ -19,6 +20,10 @@ struct hybbx_session; * Core uses TCP/IPv4+IPv6 and HBX internally. Plugins terminate the wire * to external services (telnet TCP, serial TNC, ARDOP host TCP, …) and * expose a byte stream to hybbx_session via the write callback. + * + * Optional extensions (NULL = not used): + * tick — called every ~1s from service loop (game clocks, AI, …) + * on_command — intercept "/verb" before core dispatch (entertainment, …) */ typedef struct hybbx_transport_plugin { const char *name; @@ -48,6 +53,21 @@ typedef struct hybbx_transport_plugin { hybbx_result_t (*write)(struct hybbx_session *session, const char *data, size_t len); + /** + * Periodic tick — called every ~1 second from the service loop. + * NULL → no periodic work. Used by game clocks, AI timers, etc. + */ + void (*tick)(struct hybbx_service *service); + + /** + * Command intercept — called before core command dispatch. + * Return HYBBX_OK if handled, HYBBX_ERR_NOT_FOUND to fall through. + * NULL → no command handling. + */ + hybbx_result_t (*on_command)(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd); + } hybbx_transport_plugin_t; /** diff --git a/include/hybbx/service.h b/include/hybbx/service.h index 4730b8b..b9aa7fa 100644 --- a/include/hybbx/service.h +++ b/include/hybbx/service.h @@ -15,6 +15,8 @@ #include "hybbx/storage.h" #include "hybbx/texts.h" +struct hybbx_parsed_command; + #ifdef __cplusplus extern "C" { #endif @@ -167,6 +169,14 @@ struct hybbx_session *hybbx_service_find_registered_session( struct hybbx_circuit_hub; struct hybbx_circuit_hub *hybbx_service_circuit_hub(hybbx_service_t *service); +/** + * Try plugin on_command interceptors. + * Returns HYBBX_OK if a plugin handled the command, HYBBX_ERR_NOT_FOUND otherwise. + */ +hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd); + #ifdef __cplusplus } #endif diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9736302..161861b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -42,3 +42,7 @@ endif() if(HYBBX_PLUGIN_WEBSOCKET) add_subdirectory(websocket) endif() + +if(HYBBX_PLUGIN_ENTERTAIN) + add_subdirectory(entertain) +endif() diff --git a/plugins/entertain/CMakeLists.txt b/plugins/entertain/CMakeLists.txt new file mode 100644 index 0000000..b626028 --- /dev/null +++ b/plugins/entertain/CMakeLists.txt @@ -0,0 +1,27 @@ +add_library(hybbx_plugin_entertain STATIC + entertain.c + chess.c + chess_cmd.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_entertain + PRIVATE ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_entertain + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_entertain PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_entertain HYBBX_HAVE_PLUGIN_ENTERTAIN) +hybbx_apply_hardening(hybbx_plugin_entertain) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_entertain + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/entertain/chess.c b/plugins/entertain/chess.c new file mode 100644 index 0000000..7875d3b --- /dev/null +++ b/plugins/entertain/chess.c @@ -0,0 +1,552 @@ +/* + * Entertain plugin — chess.c + * Chess game engine: board, moves, check/checkmate, SAN, tick broadcast. + */ +#include "entertain_chess.h" +#include "hybbx/session.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +static chess_game_t g_games[CHESS_GAME_MAX]; +static unsigned g_max_games = CHESS_DEFAULT_MAX; + +/* ── Session slot tracking (future use) ────────────────────── */ + +#define SESS_MAX 256u +static struct { uint64_t id; unsigned slot; } g_sess[SESS_MAX]; + +unsigned chess_session_slot(const struct hybbx_session *s) +{ + uint64_t sid; unsigned i; + if (!s) return 0; + sid = hybbx_session_id(s); + for (i = 0; i < SESS_MAX; i++) + if (g_sess[i].id == sid) return g_sess[i].slot; + return 0; +} + +void chess_session_set_slot(struct hybbx_session *s, unsigned slot) +{ + uint64_t sid; unsigned i, fi = SESS_MAX; + if (!s) return; + sid = hybbx_session_id(s); + for (i = 0; i < SESS_MAX; i++) { + if (g_sess[i].id == sid) { g_sess[i].slot = slot; return; } + if (g_sess[i].id == 0 && fi == SESS_MAX) fi = i; + } + if (slot > 0 && fi < SESS_MAX) { g_sess[fi].id = sid; g_sess[fi].slot = slot; } +} + +/* ── Board init ────────────────────────────────────────────── */ + +static void board_init(chess_board_t *b) +{ + static const char bk[] = "RNBQKBNR"; + unsigned f; + memset(b, 0, sizeof(*b)); + for (f = 0; f < 8; f++) { + b->sq[0][f] = bk[f]; + b->sq[1][f] = 'P'; + b->sq[6][f] = 'p'; + b->sq[7][f] = (char)tolower((unsigned char)bk[f]); + } + for (unsigned r = 2; r < 6; r++) + for (f = 0; f < 8; f++) + b->sq[r][f] = CHESS_EMPTY; +} + +/* ── Lifecycle ─────────────────────────────────────────────── */ + +void chess_init(unsigned max_games) +{ + unsigned i; + memset(g_games, 0, sizeof(g_games)); + memset(g_sess, 0, sizeof(g_sess)); + if (max_games == 0 || max_games > CHESS_GAME_MAX) + max_games = CHESS_DEFAULT_MAX; + g_max_games = max_games; + for (i = 0; i < g_max_games; i++) { + g_games[i].active = 1; + snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), "G%u", i + 1); + } +} + +void chess_shutdown(void) +{ + unsigned i; + for (i = 0; i < CHESS_GAME_MAX; i++) g_games[i].active = 0; +} + +unsigned chess_max_games(void) { return g_max_games; } + +/* ── Helpers ───────────────────────────────────────────────── */ + +static int parse_sq(const char *s, unsigned *r, unsigned *f) +{ + if (!s || strlen(s) < 2 || s[0] < 'a' || s[0] > 'h' || + s[1] < '1' || s[1] > '8') + return 0; + *f = (unsigned)(s[0] - 'a'); + *r = (unsigned)(s[1] - '1'); + return 1; +} + +static chess_color_t pcol(char p) +{ + if (p == CHESS_EMPTY) return CHESS_COLOR_NONE; + return (p >= 'A' && p <= 'Z') ? CHESS_WHITE : CHESS_BLACK; +} + +static char pup(char p) { return (char)toupper((unsigned char)p); } +static int inb(unsigned r, unsigned f) { return r < 8 && f < 8; } + +/* ── Check detection ───────────────────────────────────────── */ + +static int sq_attacked(const chess_board_t *b, unsigned rk, unsigned fl, + chess_color_t by) +{ + unsigned r, f; + char P, N, B, R, Q, K; + if (by == CHESS_WHITE) { P='P'; N='N'; B='B'; R='R'; Q='Q'; K='K'; } + else { P='p'; N='n'; B='b'; R='r'; Q='q'; K='k'; } + + /* Pawn */ + if (by == CHESS_WHITE) { + if (rk > 0 && fl > 0 && b->sq[rk-1][fl-1] == P) return 1; + if (rk > 0 && fl < 7 && b->sq[rk-1][fl+1] == P) return 1; + } else { + if (rk < 7 && fl > 0 && b->sq[rk+1][fl-1] == P) return 1; + if (rk < 7 && fl < 7 && b->sq[rk+1][fl+1] == P) return 1; + } + + /* Knight */ + { + static const int kr[] = {2,2,-2,-2,1,1,-1,-1}; + static const int kf[] = {1,-1,1,-1,2,-2,2,-2}; + for (unsigned i = 0; i < 8; i++) { + int nr = (int)rk + kr[i], nf = (int)fl + kf[i]; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) + if (b->sq[nr][nf] == N) return 1; + } + } + + /* Bishop/Queen diagonals */ + { + static const int dr[] = {1,1,-1,-1}, df[] = {1,-1,1,-1}; + for (unsigned d = 0; d < 4; d++) { + r = rk; f = fl; + while (1) { + r = (unsigned)((int)r + dr[d]); + f = (unsigned)((int)f + df[d]); + if (!inb(r, f)) break; + if (b->sq[r][f] != CHESS_EMPTY) { + if (b->sq[r][f] == B || b->sq[r][f] == Q) return 1; + break; + } + } + } + } + + /* Rook/Queen straights */ + { + static const int dr[] = {1,-1,0,0}, df[] = {0,0,1,-1}; + for (unsigned d = 0; d < 4; d++) { + r = rk; f = fl; + while (1) { + r = (unsigned)((int)r + dr[d]); + f = (unsigned)((int)f + df[d]); + if (!inb(r, f)) break; + if (b->sq[r][f] != CHESS_EMPTY) { + if (b->sq[r][f] == R || b->sq[r][f] == Q) return 1; + break; + } + } + } + } + + /* King */ + { + static const int dr[] = {1,1,1,0,0,-1,-1,-1}; + static const int df[] = {1,0,-1,1,-1,1,0,-1}; + for (unsigned d = 0; d < 8; d++) { + int nr = (int)rk + dr[d], nf = (int)fl + df[d]; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) + if (b->sq[nr][nf] == K) return 1; + } + } + + return 0; +} + +static int find_king(const chess_board_t *b, chess_color_t c, + unsigned *kr, unsigned *kf) +{ + char k = (c == CHESS_WHITE) ? 'K' : 'k'; + for (unsigned r = 0; r < 8; r++) + for (unsigned f = 0; f < 8; f++) + if (b->sq[r][f] == k) { *kr = r; *kf = f; return 1; } + return 0; +} + +static int in_check(const chess_board_t *b, chess_color_t side) +{ + unsigned kr, kf; + chess_color_t opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; + if (!find_king(b, side, &kr, &kf)) return 0; + return sq_attacked(b, kr, kf, opp); +} + +/* ── Move legality ─────────────────────────────────────────── */ + +static int pseudo_pawn(const chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf, chess_color_t side) +{ + int dir = (side == CHESS_WHITE) ? 1 : -1; + int start = (side == CHESS_WHITE) ? 1 : 6; + int dr = (int)tr - (int)fr, df = (int)tf - (int)ff; + if (df == 0) { + if (dr == dir && b->sq[tr][tf] == CHESS_EMPTY) return 1; + if (dr == 2*dir && (int)fr == start && + b->sq[fr+(unsigned)dir][ff] == CHESS_EMPTY && + b->sq[tr][tf] == CHESS_EMPTY) return 1; + } + if ((df == 1 || df == -1) && dr == dir && b->sq[tr][tf] != CHESS_EMPTY) + return 1; + return 0; +} + +static int pseudo_knight(unsigned fr, unsigned ff, unsigned tr, unsigned tf) +{ + int dr = abs((int)tr-(int)fr), df = abs((int)tf-(int)ff); + return (dr==2&&df==1)||(dr==1&&df==2); +} + +static int slide_clear(const chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf) +{ + int dr = 0, df = 0; + unsigned r, f; + if ((int)tr > (int)fr) dr = 1; else if ((int)tr < (int)fr) dr = -1; + if ((int)tf > (int)ff) df = 1; else if ((int)tf < (int)ff) df = -1; + r = (unsigned)((int)fr+dr); f = (unsigned)((int)ff+df); + while (r != tr || f != tf) { + if (!inb(r,f) || b->sq[r][f] != CHESS_EMPTY) return 0; + r = (unsigned)((int)r+dr); f = (unsigned)((int)f+df); + } + return 1; +} + +static int has_any_legal(chess_board_t *b, chess_color_t side); + +static int pseudo_legal(const chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf, chess_color_t side) +{ + char pc = b->sq[fr][ff]; + int dr = abs((int)tr-(int)fr), df = abs((int)tf-(int)ff); + if (pc == CHESS_EMPTY || pcol(pc) != side) return 0; + if (pcol(b->sq[tr][tf]) == side) return 0; + switch (pup(pc)) { + case 'P': return pseudo_pawn(b, fr, ff, tr, tf, side); + case 'N': return pseudo_knight(fr, ff, tr, tf); + case 'B': return dr==df && dr>0 && slide_clear(b, fr, ff, tr, tf); + case 'R': return (fr==tr||ff==tf) && slide_clear(b, fr, ff, tr, tf); + case 'Q': return ((fr==tr||ff==tf)||(dr==df)) && slide_clear(b, fr, ff, tr, tf); + case 'K': return dr<=1 && df<=1 && (dr+df>0); + } + return 0; +} + +static int is_legal(chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf, chess_color_t side) +{ + chess_board_t tmp; + if (!pseudo_legal(b, fr, ff, tr, tf, side)) return 0; + memcpy(&tmp, b, sizeof(tmp)); + tmp.sq[tr][tf] = tmp.sq[fr][ff]; + tmp.sq[fr][ff] = CHESS_EMPTY; + if (pup(tmp.sq[tr][tf]) == 'P' && + ((side == CHESS_WHITE && tr == 7) || (side == CHESS_BLACK && tr == 0))) + tmp.sq[tr][tf] = (side == CHESS_WHITE) ? 'Q' : 'q'; + return !in_check(&tmp, side); +} + +static int has_any_legal(chess_board_t *b, chess_color_t side) +{ + for (unsigned fr = 0; fr < 8; fr++) + for (unsigned ff = 0; ff < 8; ff++) { + if (pcol(b->sq[fr][ff]) != side) continue; + for (unsigned tr = 0; tr < 8; tr++) + for (unsigned tf = 0; tf < 8; tf++) + if (is_legal(b, fr, ff, tr, tf, side)) return 1; + } + return 0; +} + +/* ── SAN ───────────────────────────────────────────────────── */ + +static void make_san(char *out, size_t len, char pc, unsigned ff, + unsigned tr, unsigned tf, int cap, int chk, + int mate, char promo) +{ + int n; + if (pup(pc) == 'P') { + if (cap) n = snprintf(out, len, "%cx%c%d", 'a'+(int)ff, 'a'+(int)tf, (int)(tr+1)); + else n = snprintf(out, len, "%c%d", 'a'+(int)tf, (int)(tr+1)); + } else { + n = snprintf(out, len, "%c%c%d", pup(pc), 'a'+(int)tf, (int)(tr+1)); + } + if (promo) snprintf(out+n, len-(size_t)n, "=%c", pup(promo)); + else if (mate) snprintf(out+n, len-(size_t)n, "#"); + else if (chk) snprintf(out+n, len-(size_t)n, "+"); +} + +/* ── Game management ───────────────────────────────────────── */ + +chess_game_t *chess_get_game(unsigned i) +{ + return (i < CHESS_GAME_MAX && g_games[i].active) ? &g_games[i] : NULL; +} + +chess_game_t *chess_create_game(struct hybbx_session *white, const char *name) +{ + unsigned i; + for (i = 0; i < g_max_games; i++) { + if (!g_games[i].active) { + memset(&g_games[i], 0, sizeof(g_games[i])); + g_games[i].active = 1; + hybbx_strlcpy(g_games[i].white_name, name, CHESS_NAME_MAX); + g_games[i].white_session = white; + board_init(&g_games[i].board); + g_games[i].turn = CHESS_WHITE; + g_games[i].move_number = 1; + snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), "G%u", i+1); + hybbx_log_info("[chess] game %s created by %s", g_games[i].game_id, name); + return &g_games[i]; + } + } + return NULL; +} + +chess_game_t *chess_find_open_game(void) +{ + for (unsigned i = 0; i < g_max_games; i++) + if (g_games[i].active && !g_games[i].black_session) return &g_games[i]; + return NULL; +} + +chess_game_t *chess_find_game_by_id(const char *id) +{ + if (!id) return NULL; + for (unsigned i = 0; i < g_max_games; i++) + if (g_games[i].active && strcasecmp(g_games[i].game_id, id) == 0) + return &g_games[i]; + return NULL; +} + +int chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name) +{ + if (!g || g->black_session) return 0; + g->black_session = black; + hybbx_strlcpy(g->black_name, name, CHESS_NAME_MAX); + hybbx_log_info("[chess] %s joined %s as black", name, g->game_id); + return 1; +} + +int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec) +{ + unsigned i; + if (!g || !spec) return 0; + if (chess_is_player(g, spec)) return 0; + for (i = 0; i < g->spectator_count; i++) + if (g->spectators[i] == spec) return 1; + if (g->spectator_count >= CHESS_SPECTATOR_MAX) return 0; + g->spectators[g->spectator_count++] = spec; + return 1; +} + +void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec) +{ + unsigned i; + if (!g || !spec) return; + for (i = 0; i < g->spectator_count; i++) { + if (g->spectators[i] == spec) { + g->spectators[i] = g->spectators[--g->spectator_count]; + return; + } + } +} + +int chess_is_player(const chess_game_t *g, const struct hybbx_session *s) +{ + return g && s && (g->white_session == s || g->black_session == s); +} + +int chess_is_spectator(const chess_game_t *g, const struct hybbx_session *s) +{ + unsigned i; + if (!g || !s) return 0; + for (i = 0; i < g->spectator_count; i++) + if (g->spectators[i] == s) return 1; + return 0; +} + +chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_session *s) +{ + if (!g || !s) return CHESS_COLOR_NONE; + if (g->white_session == s) return CHESS_WHITE; + if (g->black_session == s) return CHESS_BLACK; + return CHESS_COLOR_NONE; +} + +/* ── Move execution ────────────────────────────────────────── */ + +int chess_make_move(chess_game_t *g, const char *from, const char *to, + char promote, char *err, size_t errlen) +{ + unsigned fr, ff, tr, tf; + char piece, captured; + int check, mate; + chess_color_t side, opp; + + if (!g || g->result != CHESS_RESULT_NONE) { + if (err) snprintf(err, errlen, "Game is over."); + return 0; + } + side = g->turn; + if (!parse_sq(from, &fr, &ff)) { if (err) snprintf(err, errlen, "Bad square: %s", from); return 0; } + if (!parse_sq(to, &tr, &tf)) { if (err) snprintf(err, errlen, "Bad square: %s", to); return 0; } + + piece = g->board.sq[fr][ff]; + if (pcol(piece) != side) { if (err) snprintf(err, errlen, "Not your piece."); return 0; } + if (!is_legal(&g->board, fr, ff, tr, tf, side)) { if (err) snprintf(err, errlen, "Illegal move."); return 0; } + + captured = g->board.sq[tr][tf]; + g->board.sq[tr][tf] = piece; + g->board.sq[fr][ff] = CHESS_EMPTY; + + /* Promotion */ + if (pup(piece) == 'P' && + ((side == CHESS_WHITE && tr == 7) || (side == CHESS_BLACK && tr == 0))) { + char pp = promote ? pup(promote) : 'Q'; + g->board.sq[tr][tf] = (side == CHESS_WHITE) ? pp : (char)tolower((unsigned char)pp); + } + + opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; + check = in_check(&g->board, opp); + mate = check && !has_any_legal(&g->board, opp); + + make_san(g->last_move_san, sizeof(g->last_move_san), piece, ff, tr, tf, + captured != CHESS_EMPTY, check, mate, promote); + + if (mate) g->result = (side == CHESS_WHITE) ? CHESS_WHITE_WINS : CHESS_BLACK_WINS; + else if (!has_any_legal(&g->board, opp)) g->result = CHESS_DRAW_STALEMATE; + + g->turn = opp; + if (opp == CHESS_WHITE) g->move_number++; + g->tick_counter = 0; + + hybbx_log_info("[chess] %s: %s %s", g->game_id, + (side == CHESS_WHITE) ? g->white_name : g->black_name, + g->last_move_san); + return 1; +} + +/* ── Resign / Draw ─────────────────────────────────────────── */ + +void chess_resign(chess_game_t *g, struct hybbx_session *who) +{ + if (!g || g->result != CHESS_RESULT_NONE) return; + if (g->white_session == who) g->result = CHESS_BLACK_WINS; + else if (g->black_session == who) g->result = CHESS_WHITE_WINS; +} + +void chess_offer_draw(chess_game_t *g, struct hybbx_session *who) +{ + (void)who; + if (!g || g->result != CHESS_RESULT_NONE) return; + g->result = CHESS_DRAW_AGREEMENT; +} + +/* ── Board rendering ───────────────────────────────────────── */ + +static void render_board(const chess_board_t *b, struct hybbx_session *s, + const char *label) +{ + char line[96]; + unsigned r, f; + static const char *gc = ".KQRBNPkqrbnp"; + (void)gc; + + if (label && label[0]) hybbx_session_write_line(s, label); + hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+"); + for (r = 0; r < 8; r++) { + unsigned rk = 7 - r; + int n = snprintf(line, sizeof(line), "%u |", rk + 1); + for (f = 0; f < 8; f++) { + char p = b->sq[rk][f]; + const char *ch; + switch (p) { + case 'K': ch="K"; break; case 'Q': ch="Q"; break; + case 'R': ch="R"; break; case 'B': ch="B"; break; + case 'N': ch="N"; break; case 'P': ch="P"; break; + case 'k': ch="k"; break; case 'q': ch="q"; break; + case 'r': ch="r"; break; case 'b': ch="b"; break; + case 'n': ch="n"; break; case 'p': ch="p"; break; + default: ch="."; break; + } + n += snprintf(line+n, sizeof(line)-(size_t)n, " %s |", ch); + } + hybbx_session_write_line(s, line); + hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+"); + } + hybbx_session_write_line(s, " a b c d e f g h"); +} + +void chess_send_board(chess_game_t *g, struct hybbx_session *s) +{ + char label[160]; + const char *turn; + if (!g || !s) return; + turn = (g->turn == CHESS_WHITE) ? g->white_name : g->black_name; + if (g->result != CHESS_RESULT_NONE) { + const char *rs; + switch (g->result) { + case CHESS_WHITE_WINS: rs = "1-0 White wins"; break; + case CHESS_BLACK_WINS: rs = "0-1 Black wins"; break; + case CHESS_DRAW_STALEMATE: rs = "1/2 Stalemate"; break; + case CHESS_DRAW_AGREEMENT: rs = "1/2 Draw"; break; + default: rs = "Over"; break; + } + snprintf(label, sizeof(label), "[%s] %s vs %s %s Move %u", + g->game_id, g->white_name, g->black_name, rs, g->move_number); + } else { + snprintf(label, sizeof(label), "[%s] %s(W) vs %s(B) %s to move %s Move %u", + g->game_id, g->white_name, g->black_name, turn, + g->last_move_san[0] ? g->last_move_san : "", g->move_number); + } + render_board(&g->board, s, label); +} + +/* ── Tick — 15s board broadcast ────────────────────────────── */ + +void chess_tick(struct hybbx_service *service) +{ + unsigned i, j; + (void)service; + for (i = 0; i < g_max_games; i++) { + chess_game_t *g = &g_games[i]; + if (!g->active || !g->black_session) continue; + g->tick_counter++; + if (g->tick_counter < CHESS_REFRESH_SEC) continue; + g->tick_counter = 0; + if (g->white_session) chess_send_board(g, g->white_session); + if (g->black_session) chess_send_board(g, g->black_session); + for (j = 0; j < g->spectator_count; j++) + chess_send_board(g, g->spectators[j]); + } +} diff --git a/plugins/entertain/chess_cmd.c b/plugins/entertain/chess_cmd.c new file mode 100644 index 0000000..3e57223 --- /dev/null +++ b/plugins/entertain/chess_cmd.c @@ -0,0 +1,277 @@ +/* + * Entertain plugin — chess_cmd.c + * /chess command handler (join/leave/list/move/resign/draw/clear/status/board). + */ +#include "entertain_chess.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/command.h" + +#include <stdio.h> +#include <string.h> +#include <strings.h> + +/* Forward declaration */ +hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd); + +static int ieq(const char *a, const char *b) +{ + if (!a || !b) return 0; + while (*a && *b) { + char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b; + if (ca != cb) return 0; + a++; b++; + } + return *a == *b; +} + +hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd) +{ + const char *sub; + char line[128]; + (void)service; + + if (!hybbx_session_logged_in(session)) { + hybbx_session_write_line(session, "Log in to play chess."); + return HYBBX_ERR_DENIED; + } + + sub = (cmd->argc >= 1) ? cmd->argv[0] : "help"; + + /* ── help ──────────────────────────────────────────────── */ + if (ieq(sub, "help") || ieq(sub, "?")) { + hybbx_session_write_line(session, "Chess commands:"); + hybbx_session_write_line(session, " /chess new - Create game (you are White)"); + hybbx_session_write_line(session, " /chess join [id] - Join game as Black"); + hybbx_session_write_line(session, " /chess watch [id] - Spectate"); + hybbx_session_write_line(session, " /chess board - Show board"); + hybbx_session_write_line(session, " /chess move e2 e4 - Move (alias: /mv e2 e4)"); + hybbx_session_write_line(session, " /chess resign - Resign"); + hybbx_session_write_line(session, " /chess draw - Offer draw"); + hybbx_session_write_line(session, " /chess list - List games + boards"); + hybbx_session_write_line(session, " /chess leave - Stop watching"); + hybbx_session_write_line(session, " /chess clear - Clear game history"); + snprintf(line, sizeof(line), "Board refreshes every %ds for all.", CHESS_REFRESH_SEC); + hybbx_session_write_line(session, line); + return HYBBX_OK; + } + + /* ── status ────────────────────────────────────────────── */ + if (ieq(sub, "status")) { + snprintf(line, sizeof(line), "Chess: %u max games", chess_max_games()); + hybbx_session_write_line(session, line); + return HYBBX_OK; + } + + /* ── list ──────────────────────────────────────────────── */ + if (ieq(sub, "list") || ieq(sub, "ls")) { + unsigned found = 0; + hybbx_session_write_line(session, "Chess games:"); + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (!g) continue; + const char *st = (g->result != CHESS_RESULT_NONE) ? "finished" : + (!g->black_session) ? "waiting" : "playing"; + snprintf(line, sizeof(line), " [%s] %s(W) vs %s(B) %s move %u", + g->game_id, g->white_name, + g->black_session ? g->black_name : "...", + st, g->move_number); + hybbx_session_write_line(session, line); + chess_send_board(g, session); + if (g->spectator_count > 0) { + int n = snprintf(line, sizeof(line), " Spectators (%u): ", g->spectator_count); + for (unsigned j = 0; j < g->spectator_count; j++) { + const char *nm = hybbx_session_display_name(g->spectators[j]); + if (j > 0) n += snprintf(line+n, sizeof(line)-(size_t)n, ", "); + n += snprintf(line+n, sizeof(line)-(size_t)n, "%s", nm ? nm : "?"); + } + hybbx_session_write_line(session, line); + } + hybbx_session_write_line(session, ""); + found++; + } + if (!found) hybbx_session_write_line(session, " No active games."); + return HYBBX_OK; + } + + /* ── new ───────────────────────────────────────────────── */ + if (ieq(sub, "new") || ieq(sub, "create")) { + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && chess_is_player(g, session)) { + hybbx_session_write_line(session, "Already in a game. /chess resign first."); + return HYBBX_ERR_DENIED; + } + } + chess_game_t *g = chess_create_game(session, hybbx_session_username(session)); + if (!g) { hybbx_session_write_line(session, "No free game slots."); return HYBBX_ERR_DENIED; } + snprintf(line, sizeof(line), "Game %s created. You are White.", g->game_id); + hybbx_session_write_line(session, line); + hybbx_session_write_line(session, "Waiting for Black to /chess join..."); + chess_send_board(g, session); + return HYBBX_OK; + } + + /* ── join ──────────────────────────────────────────────── */ + if (ieq(sub, "join")) { + const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && chess_is_player(g, session)) { + hybbx_session_write_line(session, "Already in a game. /chess resign first."); + return HYBBX_ERR_DENIED; + } + } + chess_game_t *g = NULL; + if (id) { + g = chess_find_game_by_id(id); + if (!g || !g->active) { hybbx_session_write_line(session, "Game not found."); return HYBBX_ERR_NOT_FOUND; } + if (g->black_session) { hybbx_session_write_line(session, "Game is full."); return HYBBX_ERR_DENIED; } + } else { + g = chess_find_open_game(); + if (!g) { hybbx_session_write_line(session, "No open games. /chess new"); return HYBBX_ERR_NOT_FOUND; } + } + chess_join_game(g, session, hybbx_session_username(session)); + snprintf(line, sizeof(line), "Joined %s as Black. Game on!", g->game_id); + hybbx_session_write_line(session, line); + chess_send_board(g, g->white_session); + chess_send_board(g, session); + return HYBBX_OK; + } + + /* ── watch ─────────────────────────────────────────────── */ + if (ieq(sub, "watch") || ieq(sub, "spectate")) { + const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + chess_game_t *g = NULL; + if (id) { + g = chess_find_game_by_id(id); + } else { + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *t = chess_get_game(i); + if (t && t->black_session) { g = t; break; } + } + } + if (!g || !g->active) { hybbx_session_write_line(session, "No game to watch."); return HYBBX_ERR_NOT_FOUND; } + if (!chess_add_spectator(g, session)) { hybbx_session_write_line(session, "Spectator list full."); return HYBBX_ERR_DENIED; } + snprintf(line, sizeof(line), "Watching %s. Board refreshes every %ds.", g->game_id, CHESS_REFRESH_SEC); + hybbx_session_write_line(session, line); + chess_send_board(g, session); + return HYBBX_OK; + } + + /* ── leave ─────────────────────────────────────────────── */ + if (ieq(sub, "leave") || ieq(sub, "unwatch")) { + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && chess_is_spectator(g, session)) { + chess_remove_spectator(g, session); + hybbx_session_write_line(session, "Stopped watching."); + return HYBBX_OK; + } + } + hybbx_session_write_line(session, "Not watching any game."); + return HYBBX_ERR_NOT_FOUND; + } + + /* ── board ─────────────────────────────────────────────── */ + if (ieq(sub, "board") || ieq(sub, "show")) { + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && (chess_is_player(g, session) || chess_is_spectator(g, session))) { + chess_send_board(g, session); + return HYBBX_OK; + } + } + hybbx_session_write_line(session, "Not in a game. /chess new, join, or watch."); + return HYBBX_ERR_NOT_FOUND; + } + + /* ── move ──────────────────────────────────────────────── */ + if (ieq(sub, "move") || ieq(sub, "mv") || ieq(sub, "m")) { + if (cmd->argc < 3) { hybbx_session_write_line(session, "Usage: /chess move e2 e4"); return HYBBX_ERR_INVALID; } + chess_game_t *game = NULL; + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && g->result == CHESS_RESULT_NONE && chess_is_player(g, session)) { + if (chess_player_color(g, session) == g->turn) { game = g; break; } + } + } + if (!game) { hybbx_session_write_line(session, "Not your turn or no active game."); return HYBBX_ERR_DENIED; } + char err[64]; + if (!chess_make_move(game, cmd->argv[1], cmd->argv[2], 0, err, sizeof(err))) { + snprintf(line, sizeof(line), "Illegal: %s", err); + hybbx_session_write_line(session, line); + return HYBBX_ERR_INVALID; + } + /* Broadcast to all */ + if (game->white_session) chess_send_board(game, game->white_session); + if (game->black_session) chess_send_board(game, game->black_session); + for (unsigned j = 0; j < game->spectator_count; j++) + chess_send_board(game, game->spectators[j]); + /* Game over? */ + if (game->result != CHESS_RESULT_NONE) { + const char *res; + switch (game->result) { + case CHESS_WHITE_WINS: res = "White wins!"; break; + case CHESS_BLACK_WINS: res = "Black wins!"; break; + case CHESS_DRAW_STALEMATE: res = "Stalemate!"; break; + case CHESS_DRAW_AGREEMENT: res = "Draw!"; break; + default: res = "Game over."; break; + } + snprintf(line, sizeof(line), "[%s] %s", game->game_id, res); + if (game->white_session) hybbx_session_write_line(game->white_session, line); + if (game->black_session) hybbx_session_write_line(game->black_session, line); + for (unsigned j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + } + return HYBBX_OK; + } + + /* ── resign ────────────────────────────────────────────── */ + if (ieq(sub, "resign")) { + chess_game_t *game = NULL; + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && chess_is_player(g, session)) { game = g; break; } + } + if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; } + chess_resign(game, session); + snprintf(line, sizeof(line), "[%s] %s resigned.", game->game_id, hybbx_session_username(session)); + if (game->white_session) hybbx_session_write_line(game->white_session, line); + if (game->black_session) hybbx_session_write_line(game->black_session, line); + for (unsigned j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + return HYBBX_OK; + } + + /* ── draw ──────────────────────────────────────────────── */ + if (ieq(sub, "draw")) { + chess_game_t *game = NULL; + for (unsigned i = 0; i < chess_max_games(); i++) { + chess_game_t *g = chess_get_game(i); + if (g && chess_is_player(g, session)) { game = g; break; } + } + if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; } + chess_offer_draw(game, session); + snprintf(line, sizeof(line), "[%s] Draw agreed.", game->game_id); + if (game->white_session) hybbx_session_write_line(game->white_session, line); + if (game->black_session) hybbx_session_write_line(game->black_session, line); + for (unsigned j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + return HYBBX_OK; + } + + /* ── clear ─────────────────────────────────────────────── */ + if (ieq(sub, "clear") || ieq(sub, "reset")) { + hybbx_session_write_line(session, "Chess has no persistent history to clear."); + return HYBBX_OK; + } + + hybbx_session_write_line(session, "Unknown chess command. /chess help"); + return HYBBX_ERR_NOT_FOUND; +} diff --git a/plugins/entertain/entertain.c b/plugins/entertain/entertain.c new file mode 100644 index 0000000..cf3e123 --- /dev/null +++ b/plugins/entertain/entertain.c @@ -0,0 +1,67 @@ +/* + * Entertain plugin — entertain.c + * Plugin registration: init, shutdown, tick, on_command. + * Chess is the first entertainment feature; more can be added. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/command.h" +#include "hybbx/log.h" +#include "entertain_chess.h" + +#include <string.h> + +static struct hybbx_service *g_service; + +/* Forward declarations from chess_cmd.c */ +extern hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd); + +static hybbx_result_t entertain_init(struct hybbx_service *service) +{ + g_service = service; + chess_init(CHESS_DEFAULT_MAX); + hybbx_log_info("[entertain] plugin loaded — chess_max_games=%u", (unsigned)CHESS_DEFAULT_MAX); + return HYBBX_OK; +} + +static void entertain_shutdown(void) +{ + chess_shutdown(); + g_service = NULL; + hybbx_log_info("[entertain] plugin unloaded"); +} + +static void entertain_tick(struct hybbx_service *service) +{ + chess_tick(service); +} + +static hybbx_result_t entertain_on_command(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd) +{ + if (cmd == NULL || cmd->verb == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (strcmp(cmd->verb, "chess") == 0 || strcmp(cmd->verb, "play") == 0) { + return entertain_cmd_chess(service, session, cmd); + } + + return HYBBX_ERR_NOT_FOUND; +} + +const hybbx_transport_plugin_t hybbx_plugin_entertain = { + .name = "entertain", + .kind = 0, /* not a transport — feature plugin */ + .version = 1, + .init = entertain_init, + .shutdown = entertain_shutdown, + .start = NULL, + .stop = NULL, + .write = NULL, + .tick = entertain_tick, + .on_command = entertain_on_command, +}; diff --git a/plugins/entertain/entertain_chess.h b/plugins/entertain/entertain_chess.h new file mode 100644 index 0000000..9d7026c --- /dev/null +++ b/plugins/entertain/entertain_chess.h @@ -0,0 +1,88 @@ +/* + * Entertain plugin — chess.h (internal) + * Chess game engine for HyBBX Entertain area. + */ +#ifndef ENTERTAIN_CHESS_H +#define ENTERTAIN_CHESS_H + +#include "hybbx/types.h" + +struct hybbx_session; +struct hybbx_service; + +#define CHESS_BOARD_SIZE 8 +#define CHESS_GAME_MAX 16 +#define CHESS_SPECTATOR_MAX 32 +#define CHESS_REFRESH_SEC 15 +#define CHESS_DEFAULT_MAX 8 +#define CHESS_NAME_MAX 32 +#define CHESS_SAN_MAX 8 +#define CHESS_EMPTY '.' + +typedef enum { + CHESS_COLOR_NONE = 0, + CHESS_WHITE, + CHESS_BLACK +} chess_color_t; + +typedef enum { + CHESS_RESULT_NONE = 0, + CHESS_WHITE_WINS, + CHESS_BLACK_WINS, + CHESS_DRAW_STALEMATE, + CHESS_DRAW_AGREEMENT +} chess_result_t; + +typedef struct chess_board { + char sq[CHESS_BOARD_SIZE][CHESS_BOARD_SIZE]; +} chess_board_t; + +typedef struct chess_game { + int active; + char white_name[CHESS_NAME_MAX]; + char black_name[CHESS_NAME_MAX]; + struct hybbx_session *white_session; + struct hybbx_session *black_session; + struct hybbx_session *spectators[CHESS_SPECTATOR_MAX]; + unsigned spectator_count; + chess_board_t board; + chess_color_t turn; + unsigned move_number; + chess_result_t result; + char last_move_san[CHESS_SAN_MAX]; + unsigned tick_counter; + char game_id[8]; +} chess_game_t; + +/* Lifecycle */ +void chess_init(unsigned max_games); +void chess_shutdown(void); +unsigned chess_max_games(void); + +/* Game management */ +chess_game_t *chess_create_game(struct hybbx_session *white, const char *name); +chess_game_t *chess_find_open_game(void); +chess_game_t *chess_find_game_by_id(const char *id); +chess_game_t *chess_get_game(unsigned index); +int chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name); +int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec); +void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec); +int chess_is_player(const chess_game_t *g, const struct hybbx_session *s); +int chess_is_spectator(const chess_game_t *g, const struct hybbx_session *s); +chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_session *s); + +/* Session slot (for future use) */ +unsigned chess_session_slot(const struct hybbx_session *s); +void chess_session_set_slot(struct hybbx_session *s, unsigned slot); + +/* Moves + display */ +int chess_make_move(chess_game_t *g, const char *from, const char *to, + char promote, char *err, size_t errlen); +void chess_send_board(chess_game_t *g, struct hybbx_session *s); +void chess_resign(chess_game_t *g, struct hybbx_session *who); +void chess_offer_draw(chess_game_t *g, struct hybbx_session *who); + +/* Tick — call from plugin tick every 1s */ +void chess_tick(struct hybbx_service *service); + +#endif /* ENTERTAIN_CHESS_H */ diff --git a/share/commands.yaml b/share/commands.yaml index 5a5d848..c64352e 100644 --- a/share/commands.yaml +++ b/share/commands.yaml @@ -310,8 +310,10 @@ commands: chess: group: entertain - min: User + min: Guest line1: "/chess new|join|watch|move|resign|draw|list|board" line2: "Help: Play chess. Two players + spectators. More: /play /mv" + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e966362..9b71bcc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,8 +40,6 @@ if(HYBBX_BUILD_DAEMON) core/util.c core/log.c core/monitor.c - core/chess.c - core/chess_cmd.c core/max25.c core/security.c core/security_ban.c diff --git a/src/core/chess.c b/src/core/chess.c deleted file mode 100644 index 94d35a6..0000000 --- a/src/core/chess.c +++ /dev/null @@ -1,668 +0,0 @@ -#if defined(__linux__) -#define _DEFAULT_SOURCE -#endif - -#include "hybbx/chess.h" -#include "hybbx/session.h" -#include "hybbx/service.h" -#include "hybbx/log.h" -#include "hybbx/util.h" -#include "hybbx/config.h" - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <ctype.h> - -static chess_game_t g_games[CHESS_GAME_MAX]; -static unsigned g_max_games = CHESS_DEFAULT_MAX; - -/* ── Board init ────────────────────────────────────────────── */ - -static void board_init(chess_board_t *b) -{ - static const char back[] = "RNBQKBNR"; - unsigned f; - - memset(b, 0, sizeof(*b)); - for (f = 0; f < 8; f++) { - b->sq[0][f] = back[f]; - b->sq[1][f] = 'P'; - b->sq[6][f] = 'p'; - b->sq[7][f] = (char)tolower((unsigned char)back[f]); - } - for (unsigned r = 2; r < 6; r++) - for (f = 0; f < 8; f++) - b->sq[r][f] = CHESS_EMPTY; -} - -void hybbx_chess_init(void) -{ - memset(g_games, 0, sizeof(g_games)); - g_max_games = CHESS_DEFAULT_MAX; -} - -void hybbx_chess_shutdown(void) -{ - unsigned i; - for (i = 0; i < g_max_games; i++) - g_games[i].active = 0; -} - -void hybbx_chess_config_apply(const struct hybbx_config *config) -{ - unsigned val; - - if (config != NULL) { - val = hybbx_config_get_uint(config, "chess", "max_games", - CHESS_DEFAULT_MAX, 1, CHESS_GAME_MAX); - } else { - val = CHESS_DEFAULT_MAX; - } - g_max_games = val; - hybbx_log_info("[chess] max_games=%u", g_max_games); -} - -unsigned hybbx_chess_max_games(void) -{ - return g_max_games; -} - -/* ── Helpers ───────────────────────────────────────────────── */ - -static int parse_square(const char *s, unsigned *rank, unsigned *file) -{ - if (s == NULL || strlen(s) < 2) - return 0; - if (s[0] < 'a' || s[0] > 'h' || s[1] < '1' || s[1] > '8') - return 0; - *file = (unsigned)(s[0] - 'a'); - *rank = (unsigned)(s[1] - '1'); - return 1; -} - -static chess_color_t piece_color(char p) -{ - if (p == CHESS_EMPTY) return CHESS_COLOR_NONE; - return (p >= 'A' && p <= 'Z') ? CHESS_WHITE : CHESS_BLACK; -} - -static char piece_upper(char p) -{ - return (char)toupper((unsigned char)p); -} - -static int in_bounds(unsigned r, unsigned f) -{ - return r < 8 && f < 8; -} - -/* ── Check detection ───────────────────────────────────────── */ - -static int square_attacked_by(const chess_board_t *b, unsigned rank, - unsigned file, chess_color_t by) -{ - unsigned r, f; - int dr, df; - char pawn, knight, bishop, rook, queen, king; - - if (by == CHESS_WHITE) { - pawn = 'P'; knight = 'N'; bishop = 'B'; - rook = 'R'; queen = 'Q'; king = 'K'; - } else { - pawn = 'p'; knight = 'n'; bishop = 'b'; - rook = 'r'; queen = 'q'; king = 'k'; - } - - /* Pawn attacks */ - if (by == CHESS_WHITE) { - if (rank > 0 && file > 0 && b->sq[rank - 1][file - 1] == pawn) return 1; - if (rank > 0 && file < 7 && b->sq[rank - 1][file + 1] == pawn) return 1; - } else { - if (rank < 7 && file > 0 && b->sq[rank + 1][file - 1] == pawn) return 1; - if (rank < 7 && file < 7 && b->sq[rank + 1][file + 1] == pawn) return 1; - } - - /* Knight */ - { - static const int kr[] = {2,2,-2,-2,1,1,-1,-1}; - static const int kf[] = {1,-1,1,-1,2,-2,2,-2}; - for (unsigned i = 0; i < 8; i++) { - int nr = (int)rank + kr[i], nf = (int)file + kf[i]; - if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) - if (b->sq[nr][nf] == knight) return 1; - } - } - - /* Sliding: bishop/queen diagonals */ - { - static const int drs[] = {1,1,-1,-1}; - static const int dfs[] = {1,-1,1,-1}; - for (unsigned d = 0; d < 4; d++) { - dr = drs[d]; df = dfs[d]; - r = rank; f = file; - while (1) { - r = (unsigned)((int)r + dr); - f = (unsigned)((int)f + df); - if (!in_bounds(r, f)) break; - if (b->sq[r][f] != CHESS_EMPTY) { - if (b->sq[r][f] == bishop || b->sq[r][f] == queen) return 1; - break; - } - } - } - } - - /* Sliding: rook/queen straights */ - { - static const int drs[] = {1,-1,0,0}; - static const int dfs[] = {0,0,1,-1}; - for (unsigned d = 0; d < 4; d++) { - dr = drs[d]; df = dfs[d]; - r = rank; f = file; - while (1) { - r = (unsigned)((int)r + dr); - f = (unsigned)((int)f + df); - if (!in_bounds(r, f)) break; - if (b->sq[r][f] != CHESS_EMPTY) { - if (b->sq[r][f] == rook || b->sq[r][f] == queen) return 1; - break; - } - } - } - } - - /* King */ - { - static const int drs[] = {1,1,1,0,0,-1,-1,-1}; - static const int dfs[] = {1,0,-1,1,-1,1,0,-1}; - for (unsigned d = 0; d < 8; d++) { - int nr = (int)rank + drs[d], nf = (int)file + dfs[d]; - if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) - if (b->sq[nr][nf] == king) return 1; - } - } - - return 0; -} - -static int find_king(const chess_board_t *b, chess_color_t color, - unsigned *kr, unsigned *kf) -{ - char king = (color == CHESS_WHITE) ? 'K' : 'k'; - for (unsigned r = 0; r < 8; r++) - for (unsigned f = 0; f < 8; f++) - if (b->sq[r][f] == king) { *kr = r; *kf = f; return 1; } - return 0; -} - -static int is_in_check(const chess_board_t *b, chess_color_t side) -{ - unsigned kr, kf; - chess_color_t opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; - if (!find_king(b, side, &kr, &kf)) return 0; - return square_attacked_by(b, kr, kf, opp); -} - -/* ── Move legality ─────────────────────────────────────────── */ - -static int pseudo_legal_pawn(const chess_board_t *b, unsigned fr, - unsigned ff, unsigned tr, unsigned tf, - chess_color_t side) -{ - int dir = (side == CHESS_WHITE) ? 1 : -1; - int start_rank = (side == CHESS_WHITE) ? 1 : 6; - int dr = (int)tr - (int)fr; - int df = (int)tf - (int)ff; - - if (df == 0) { - if (dr == dir && b->sq[tr][tf] == CHESS_EMPTY) return 1; - if (dr == 2 * dir && (int)fr == start_rank && - b->sq[fr + (unsigned)dir][ff] == CHESS_EMPTY && - b->sq[tr][tf] == CHESS_EMPTY) return 1; - } - if ((df == 1 || df == -1) && dr == dir) { - if (b->sq[tr][tf] != CHESS_EMPTY) return 1; - } - return 0; -} - -static int pseudo_legal_knight(unsigned fr, unsigned ff, unsigned tr, - unsigned tf) -{ - int dr = abs((int)tr - (int)fr); - int df = abs((int)tf - (int)ff); - return (dr == 2 && df == 1) || (dr == 1 && df == 2); -} - -static int sliding_clear(const chess_board_t *b, unsigned fr, unsigned ff, - unsigned tr, unsigned tf) -{ - int dr = 0, df = 0; - unsigned r, f; - - if ((int)tr > (int)fr) dr = 1; - else if ((int)tr < (int)fr) dr = -1; - if ((int)tf > (int)ff) df = 1; - else if ((int)tf < (int)ff) df = -1; - - r = (unsigned)((int)fr + dr); - f = (unsigned)((int)ff + df); - while (r != tr || f != tf) { - if (!in_bounds(r, f) || b->sq[r][f] != CHESS_EMPTY) return 0; - r = (unsigned)((int)r + dr); - f = (unsigned)((int)f + df); - } - return 1; -} - -static int pseudo_legal(const chess_board_t *b, unsigned fr, unsigned ff, - unsigned tr, unsigned tf, chess_color_t side) -{ - char piece = b->sq[fr][ff]; - int dr = abs((int)tr - (int)fr); - int df = abs((int)tf - (int)ff); - chess_color_t target_color; - - if (piece == CHESS_EMPTY) return 0; - if (piece_color(piece) != side) return 0; - - target_color = piece_color(b->sq[tr][tf]); - if (target_color == side) return 0; - - switch (piece_upper(piece)) { - case 'P': return pseudo_legal_pawn(b, fr, ff, tr, tf, side); - case 'N': return pseudo_legal_knight(fr, ff, tr, tf); - case 'B': - if (dr != df || dr == 0) return 0; - return sliding_clear(b, fr, ff, tr, tf); - case 'R': - if (fr != tr && ff != tf) return 0; - return sliding_clear(b, fr, ff, tr, tf); - case 'Q': - if (fr != tr && ff != tf && dr != df) return 0; - return sliding_clear(b, fr, ff, tr, tf); - case 'K': - return dr <= 1 && df <= 1 && (dr + df > 0); - } - return 0; -} - -static int has_any_legal_move(chess_board_t *b, chess_color_t side); - -static int is_legal_move(chess_board_t *b, unsigned fr, unsigned ff, - unsigned tr, unsigned tf, chess_color_t side) -{ - chess_board_t tmp; - - if (!pseudo_legal(b, fr, ff, tr, tf, side)) return 0; - - memcpy(&tmp, b, sizeof(tmp)); - tmp.sq[tr][tf] = tmp.sq[fr][ff]; - tmp.sq[fr][ff] = CHESS_EMPTY; - - /* Auto-promote to queen */ - if (piece_upper(tmp.sq[tr][tf]) == 'P') { - if ((side == CHESS_WHITE && tr == 7) || - (side == CHESS_BLACK && tr == 0)) { - tmp.sq[tr][tf] = (side == CHESS_WHITE) ? 'Q' : 'q'; - } - } - - return !is_in_check(&tmp, side); -} - -static int has_any_legal_move(chess_board_t *b, chess_color_t side) -{ - for (unsigned fr = 0; fr < 8; fr++) - for (unsigned ff = 0; ff < 8; ff++) { - if (piece_color(b->sq[fr][ff]) != side) continue; - for (unsigned tr = 0; tr < 8; tr++) - for (unsigned tf = 0; tf < 8; tf++) - if (is_legal_move(b, fr, ff, tr, tf, side)) - return 1; - } - return 0; -} - -/* ── SAN notation (simplified) ─────────────────────────────── */ - -static void make_san(char *out, size_t outlen, char piece, - unsigned ff, unsigned tr, unsigned tf, - int capture, int check, int mate, char promote) -{ - char *p = out; - size_t room = outlen; - - if (piece_upper(piece) == 'P') { - if (capture) { - int n = snprintf(p, room, "%cx%c%d", 'a' + ff, 'a' + tf, tr + 1); - p += n; room -= (size_t)n; - } else { - int n = snprintf(p, room, "%c%d", 'a' + tf, tr + 1); - p += n; room -= (size_t)n; - } - } else { - int n = snprintf(p, room, "%c%c%d", piece_upper(piece), - 'a' + tf, tr + 1); - p += n; room -= (size_t)n; - } - if (promote) { - snprintf(p, room, "=%c", piece_upper(promote)); - } else if (mate) { - snprintf(p, room, "#"); - } else if (check) { - snprintf(p, room, "+"); - } -} - -/* ── Game management ───────────────────────────────────────── */ - -chess_game_t *hybbx_chess_get_game(unsigned index) -{ - if (index >= CHESS_GAME_MAX) return NULL; - return &g_games[index]; -} - -chess_game_t *hybbx_chess_create_game(struct hybbx_session *white, - const char *white_name) -{ - unsigned i; - for (i = 0; i < g_max_games; i++) { - if (!g_games[i].active) { - memset(&g_games[i], 0, sizeof(g_games[i])); - g_games[i].active = 1; - hybbx_strlcpy(g_games[i].white_name, white_name, - sizeof(g_games[i].white_name)); - g_games[i].white_session = white; - board_init(&g_games[i].board); - g_games[i].turn = CHESS_WHITE; - g_games[i].move_number = 1; - snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), - "G%u", i + 1); - hybbx_log_info("[chess] game %s created by %s", - g_games[i].game_id, white_name); - return &g_games[i]; - } - } - return NULL; -} - -chess_game_t *hybbx_chess_find_open_game(void) -{ - unsigned i; - for (i = 0; i < g_max_games; i++) - if (g_games[i].active && g_games[i].black_session == NULL) - return &g_games[i]; - return NULL; -} - -chess_game_t *hybbx_chess_find_game_by_id(const char *id) -{ - unsigned i; - if (id == NULL) return NULL; - for (i = 0; i < g_max_games; i++) - if (g_games[i].active && strcasecmp(g_games[i].game_id, id) == 0) - return &g_games[i]; - return NULL; -} - -int hybbx_chess_join_game(chess_game_t *game, struct hybbx_session *black, - const char *black_name) -{ - if (game == NULL || game->black_session != NULL) return 0; - game->black_session = black; - hybbx_strlcpy(game->black_name, black_name, sizeof(game->black_name)); - hybbx_log_info("[chess] %s joined game %s as black", black_name, - game->game_id); - return 1; -} - -int hybbx_chess_add_spectator(chess_game_t *game, - struct hybbx_session *spec) -{ - unsigned i; - if (game == NULL || spec == NULL) return 0; - if (hybbx_chess_is_player(game, spec)) return 0; - for (i = 0; i < game->spectator_count; i++) - if (game->spectators[i] == spec) return 1; - if (game->spectator_count >= CHESS_SPECTATOR_MAX) return 0; - game->spectators[game->spectator_count++] = spec; - return 1; -} - -void hybbx_chess_remove_spectator(chess_game_t *game, - struct hybbx_session *spec) -{ - unsigned i; - if (game == NULL || spec == NULL) return; - for (i = 0; i < game->spectator_count; i++) { - if (game->spectators[i] == spec) { - game->spectators[i] = game->spectators[--game->spectator_count]; - return; - } - } -} - -int hybbx_chess_is_player(const chess_game_t *game, - const struct hybbx_session *session) -{ - if (game == NULL || session == NULL) return 0; - return game->white_session == session || game->black_session == session; -} - -int hybbx_chess_is_spectator(const chess_game_t *game, - const struct hybbx_session *session) -{ - unsigned i; - if (game == NULL || session == NULL) return 0; - for (i = 0; i < game->spectator_count; i++) - if (game->spectators[i] == session) return 1; - return 0; -} - -chess_color_t hybbx_chess_player_color(const chess_game_t *game, - const struct hybbx_session *session) -{ - if (game == NULL || session == NULL) return CHESS_COLOR_NONE; - if (game->white_session == session) return CHESS_WHITE; - if (game->black_session == session) return CHESS_BLACK; - return CHESS_COLOR_NONE; -} - -/* ── Move execution ────────────────────────────────────────── */ - -int hybbx_chess_make_move(chess_game_t *game, const char *from, - const char *to, char promote, char *err, - size_t errlen) -{ - unsigned fr, ff, tr, tf; - char piece, captured; - int check, mate; - chess_color_t side, opp; - - if (game == NULL || game->result != CHESS_RESULT_NONE) { - if (err) snprintf(err, errlen, "Game is over."); - return 0; - } - - side = game->turn; - - if (!parse_square(from, &fr, &ff)) { - if (err) snprintf(err, errlen, "Invalid square: %s", from); - return 0; - } - if (!parse_square(to, &tr, &tf)) { - if (err) snprintf(err, errlen, "Invalid square: %s", to); - return 0; - } - - piece = game->board.sq[fr][ff]; - if (piece_color(piece) != side) { - if (err) snprintf(err, errlen, "Not your piece."); - return 0; - } - - if (!is_legal_move(&game->board, fr, ff, tr, tf, side)) { - if (err) snprintf(err, errlen, "Illegal move."); - return 0; - } - - captured = game->board.sq[tr][tf]; - game->board.sq[tr][tf] = piece; - game->board.sq[fr][ff] = CHESS_EMPTY; - - /* Pawn promotion */ - if (piece_upper(piece) == 'P') { - if ((side == CHESS_WHITE && tr == 7) || - (side == CHESS_BLACK && tr == 0)) { - char pp = promote ? (char)toupper((unsigned char)promote) : 'Q'; - game->board.sq[tr][tf] = (side == CHESS_WHITE) ? pp : - (char)tolower((unsigned char)pp); - } - } - - opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; - check = is_in_check(&game->board, opp); - mate = check && !has_any_legal_move(&game->board, opp); - - make_san(game->last_move_san, sizeof(game->last_move_san), piece, - ff, tr, tf, captured != CHESS_EMPTY, check, mate, promote); - - if (mate) { - game->result = (side == CHESS_WHITE) ? CHESS_WHITE_WINS : - CHESS_BLACK_WINS; - } else if (!has_any_legal_move(&game->board, opp)) { - game->result = CHESS_DRAW_STALEMATE; - } - - game->turn = opp; - if (opp == CHESS_WHITE) game->move_number++; - game->tick_counter = 0; - - hybbx_log_info("[chess] %s: %s %s", game->game_id, - (side == CHESS_WHITE) ? game->white_name : game->black_name, - game->last_move_san); - return 1; -} - -/* ── Resign / Draw ─────────────────────────────────────────── */ - -void hybbx_chess_resign(chess_game_t *game, struct hybbx_session *who) -{ - if (game == NULL || game->result != CHESS_RESULT_NONE) return; - if (game->white_session == who) - game->result = CHESS_BLACK_WINS; - else if (game->black_session == who) - game->result = CHESS_WHITE_WINS; -} - -void hybbx_chess_offer_draw(chess_game_t *game, struct hybbx_session *who) -{ - (void)who; - if (game == NULL || game->result != CHESS_RESULT_NONE) return; - game->result = CHESS_DRAW_AGREEMENT; -} - -/* ── Board rendering ───────────────────────────────────────── */ - -void hybbx_chess_render_board(const chess_board_t *board, - struct hybbx_session *session, - const char *label) -{ - static const char *glyph = ".KQRBNPkqrbnp"; - char line[96]; - unsigned r, f; - - if (label && label[0]) - hybbx_session_write_line(session, label); - - hybbx_session_write_line(session, - " +---+---+---+---+---+---+---+---+"); - - for (r = 0; r < 8; r++) { - unsigned rank = 7 - r; - int n = snprintf(line, sizeof(line), "%u |", rank + 1); - for (f = 0; f < 8; f++) { - char p = board->sq[rank][f]; - const char *ch; - switch (p) { - case 'K': ch = "K"; break; case 'Q': ch = "Q"; break; - case 'R': ch = "R"; break; case 'B': ch = "B"; break; - case 'N': ch = "N"; break; case 'P': ch = "P"; break; - case 'k': ch = "k"; break; case 'q': ch = "q"; break; - case 'r': ch = "r"; break; case 'b': ch = "b"; break; - case 'n': ch = "n"; break; case 'p': ch = "p"; break; - default: ch = "."; break; - } - n += snprintf(line + n, sizeof(line) - (size_t)n, " %s |", ch); - } - hybbx_session_write_line(session, line); - hybbx_session_write_line(session, - " +---+---+---+---+---+---+---+---+"); - } - hybbx_session_write_line(session, - " a b c d e f g h"); -} - -void hybbx_chess_send_board(chess_game_t *game, - struct hybbx_session *session) -{ - char label[96]; - const char *turn_name; - - if (game == NULL || session == NULL) return; - - turn_name = (game->turn == CHESS_WHITE) ? game->white_name : - game->black_name; - - if (game->result != CHESS_RESULT_NONE) { - const char *result_str; - switch (game->result) { - case CHESS_WHITE_WINS: result_str = "1-0 White wins"; break; - case CHESS_BLACK_WINS: result_str = "0-1 Black wins"; break; - case CHESS_DRAW_STALEMATE: result_str = "1/2-1/2 Stalemate"; break; - case CHESS_DRAW_AGREEMENT: result_str = "1/2-1/2 Draw"; break; - default: result_str = "Game over"; break; - } - snprintf(label, sizeof(label), "[%s] %s vs %s %s Move %u", - game->game_id, game->white_name, game->black_name, - result_str, game->move_number); - } else { - snprintf(label, sizeof(label), - "[%s] %s(W) vs %s(B) %s to move %s Move %u", - game->game_id, game->white_name, game->black_name, - turn_name, - game->last_move_san[0] ? game->last_move_san : "", - game->move_number); - } - - hybbx_chess_render_board(&game->board, session, label); -} - -/* ── 15-second broadcast tick ──────────────────────────────── */ - -void hybbx_chess_tick(struct hybbx_service *service) -{ - unsigned i, j; - - (void)service; - - for (i = 0; i < g_max_games; i++) { - chess_game_t *g = &g_games[i]; - - if (!g->active) continue; - if (g->black_session == NULL) continue; - - g->tick_counter++; - if (g->tick_counter < CHESS_REFRESH_SEC) continue; - g->tick_counter = 0; - - if (g->white_session) - hybbx_chess_send_board(g, g->white_session); - if (g->black_session) - hybbx_chess_send_board(g, g->black_session); - for (j = 0; j < g->spectator_count; j++) - hybbx_chess_send_board(g, g->spectators[j]); - } -} diff --git a/src/core/chess_cmd.c b/src/core/chess_cmd.c deleted file mode 100644 index 6ad1058..0000000 --- a/src/core/chess_cmd.c +++ /dev/null @@ -1,397 +0,0 @@ -/* ── Chess command handler ──────────────────────────────────── */ - -#include "hybbx/chess.h" -#include "hybbx/session.h" -#include "hybbx/service.h" -#include "hybbx/hybbx.h" - -#include <string.h> -#include <strings.h> -#include <stdio.h> -#include <ctype.h> - -static int str_ieq(const char *a, const char *b) -{ - if (a == NULL || b == NULL) return 0; - while (*a != '\0' && *b != '\0') { - char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); - char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); - if (ca != cb) return 0; - a++; b++; - } - return *a == '\0' && *b == '\0'; -} - -hybbx_result_t cmd_chess(hybbx_service_t *service, - hybbx_session_t *session, - const hybbx_parsed_command_t *cmd) -{ - const char *sub; - chess_game_t *game; - char line[128]; - - (void)service; - - if (!hybbx_session_logged_in(session)) { - hybbx_session_write_line(session, "Log in to play chess."); - return HYBBX_ERR_DENIED; - } - - sub = (cmd->argc >= 1) ? cmd->argv[0] : "help"; - - /* ── /chess help ───────────────────────────────────────── */ - if (str_ieq(sub, "help") || str_ieq(sub, "?")) { - hybbx_session_write_line(session, "Chess commands:"); - hybbx_session_write_line(session, - " /chess new - Create a new game (you are White)"); - hybbx_session_write_line(session, - " /chess join - Join an open game as Black"); - hybbx_session_write_line(session, - " /chess join G2 - Join a specific game by ID"); - hybbx_session_write_line(session, - " /chess watch - Watch a game as spectator"); - hybbx_session_write_line(session, - " /chess watch G2 - Watch a specific game"); - hybbx_session_write_line(session, - " /chess board - Show current board"); - hybbx_session_write_line(session, - " /chess move e2 e4 - Move piece (alias: /mv e2 e4)"); - hybbx_session_write_line(session, - " /chess resign - Resign the game"); - hybbx_session_write_line(session, - " /chess draw - Offer/accept draw"); - hybbx_session_write_line(session, - " /chess list - List games with board + spectators"); - hybbx_session_write_line(session, - " /chess leave - Stop watching"); - hybbx_session_write_line(session, - "Board auto-refreshes every 15 seconds for all."); - return HYBBX_OK; - } - - /* ── /chess list ───────────────────────────────────────── */ - if (str_ieq(sub, "list") || str_ieq(sub, "ls")) { - unsigned found = 0; - unsigned i; - for (i = 0; i < hybbx_chess_max_games(); i++) { - game = hybbx_chess_get_game(i); - if (game == NULL || !game->active) continue; - - /* Header: game id, players, state */ - { - const char *state = - (game->result != CHESS_RESULT_NONE) ? "finished" : - (game->black_session == NULL) ? "waiting for Black" : "playing"; - snprintf(line, sizeof(line), - "[%s] %s(W) vs %s(B) %s move %u", - game->game_id, game->white_name, - game->black_session ? game->black_name : "...", - state, game->move_number); - } - hybbx_session_write_line(session, line); - - /* Board */ - hybbx_chess_send_board(game, session); - - /* Spectators */ - if (game->spectator_count > 0) { - unsigned j; - int n = snprintf(line, sizeof(line), " Spectators (%u): ", - game->spectator_count); - for (j = 0; j < game->spectator_count; j++) { - const char *sname = hybbx_session_display_name( - game->spectators[j]); - if (j > 0) - n += snprintf(line + n, sizeof(line) - (size_t)n, ", "); - n += snprintf(line + n, sizeof(line) - (size_t)n, "%s", - sname ? sname : "?"); - } - hybbx_session_write_line(session, line); - } - - hybbx_session_write_line(session, ""); - found++; - } - if (!found) - hybbx_session_write_line(session, " No active games."); - return HYBBX_OK; - } - - /* ── /chess new ────────────────────────────────────────── */ - if (str_ieq(sub, "new") || str_ieq(sub, "create")) { - unsigned i; - for (i = 0; i < hybbx_chess_max_games(); i++) { - game = hybbx_chess_get_game(i); - if (game && game->active && - hybbx_chess_is_player(game, session)) { - hybbx_session_write_line(session, - "You are already in a game. /chess resign first."); - return HYBBX_ERR_DENIED; - } - } - game = hybbx_chess_create_game(session, - hybbx_session_username(session)); - if (game == NULL) { - hybbx_session_write_line(session, "No free game slots."); - return HYBBX_ERR_DENIED; - } - snprintf(line, sizeof(line), "Game %s created. You are White.", - game->game_id); - hybbx_session_write_line(session, line); - hybbx_session_write_line(session, - "Waiting for Black to /chess join..."); - hybbx_chess_send_board(game, session); - return HYBBX_OK; - } - - /* ── /chess join [id] ──────────────────────────────────── */ - if (str_ieq(sub, "join")) { - const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; - unsigned i; - - for (i = 0; i < hybbx_chess_max_games(); i++) { - game = hybbx_chess_get_game(i); - if (game && game->active && - hybbx_chess_is_player(game, session)) { - hybbx_session_write_line(session, - "You are already in a game. /chess resign first."); - return HYBBX_ERR_DENIED; - } - } - - if (id) { - game = hybbx_chess_find_game_by_id(id); - if (game == NULL || !game->active) { - hybbx_session_write_line(session, "Game not found."); - return HYBBX_ERR_NOT_FOUND; - } - if (game->black_session != NULL) { - hybbx_session_write_line(session, "Game is full."); - return HYBBX_ERR_DENIED; - } - } else { - game = hybbx_chess_find_open_game(); - if (game == NULL) { - hybbx_session_write_line(session, - "No open games. /chess new to create one."); - return HYBBX_ERR_NOT_FOUND; - } - } - - hybbx_chess_join_game(game, session, - hybbx_session_username(session)); - snprintf(line, sizeof(line), "Joined %s as Black. Game on!", - game->game_id); - hybbx_session_write_line(session, line); - hybbx_chess_send_board(game, game->white_session); - hybbx_chess_send_board(game, session); - return HYBBX_OK; - } - - /* ── /chess watch [id] ─────────────────────────────────── */ - if (str_ieq(sub, "watch") || str_ieq(sub, "spectate")) { - const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; - unsigned i; - - if (id) { - game = hybbx_chess_find_game_by_id(id); - } else { - game = NULL; - for (i = 0; i < hybbx_chess_max_games(); i++) { - chess_game_t *g = hybbx_chess_get_game(i); - if (g && g->active && g->black_session != NULL) { - game = g; - break; - } - } - } - if (game == NULL || !game->active) { - hybbx_session_write_line(session, "No game to watch."); - return HYBBX_ERR_NOT_FOUND; - } - if (!hybbx_chess_add_spectator(game, session)) { - hybbx_session_write_line(session, "Spectator list full."); - return HYBBX_ERR_DENIED; - } - snprintf(line, sizeof(line), - "Watching %s. Board refreshes every %ds.", - game->game_id, CHESS_REFRESH_SEC); - hybbx_session_write_line(session, line); - hybbx_chess_send_board(game, session); - return HYBBX_OK; - } - - /* ── /chess leave ──────────────────────────────────────── */ - if (str_ieq(sub, "leave") || str_ieq(sub, "unwatch")) { - unsigned i; - for (i = 0; i < hybbx_chess_max_games(); i++) { - game = hybbx_chess_get_game(i); - if (game && game->active && - hybbx_chess_is_spectator(game, session)) { - hybbx_chess_remove_spectator(game, session); - hybbx_session_write_line(session, "Stopped watching."); - return HYBBX_OK; - } - } - hybbx_session_write_line(session, - "You are not watching any game."); - return HYBBX_ERR_NOT_FOUND; - } - - /* ── /chess board ──────────────────────────────────────── */ - if (str_ieq(sub, "board") || str_ieq(sub, "show")) { - unsigned i; - game = NULL; - for (i = 0; i < hybbx_chess_max_games(); i++) { - chess_game_t *g = hybbx_chess_get_game(i); - if (g && g->active && - (hybbx_chess_is_player(g, session) || - hybbx_chess_is_spectator(g, session))) { - game = g; - break; - } - } - if (game == NULL) { - hybbx_session_write_line(session, - "Not in a game. /chess new, join, or watch."); - return HYBBX_ERR_NOT_FOUND; - } - hybbx_chess_send_board(game, session); - return HYBBX_OK; - } - - /* ── /chess move e2 e4 ─────────────────────────────────── */ - if (str_ieq(sub, "move") || str_ieq(sub, "mv") || str_ieq(sub, "m")) { - const char *from, *to; - char err[64]; - unsigned i; - - if (cmd->argc < 3) { - hybbx_session_write_line(session, - "Usage: /chess move e2 e4"); - return HYBBX_ERR_INVALID; - } - from = cmd->argv[1]; - to = cmd->argv[2]; - - game = NULL; - for (i = 0; i < hybbx_chess_max_games(); i++) { - chess_game_t *g = hybbx_chess_get_game(i); - if (g && g->active && g->result == CHESS_RESULT_NONE && - hybbx_chess_is_player(g, session)) { - chess_color_t color = hybbx_chess_player_color(g, session); - if (color == g->turn) { - game = g; - break; - } - } - } - if (game == NULL) { - hybbx_session_write_line(session, - "Not your turn or no active game."); - return HYBBX_ERR_DENIED; - } - - if (!hybbx_chess_make_move(game, from, to, 0, err, sizeof(err))) { - snprintf(line, sizeof(line), "Illegal: %s", err); - hybbx_session_write_line(session, line); - return HYBBX_ERR_INVALID; - } - - /* Broadcast to all */ - if (game->white_session) - hybbx_chess_send_board(game, game->white_session); - if (game->black_session) - hybbx_chess_send_board(game, game->black_session); - { - unsigned j; - for (j = 0; j < game->spectator_count; j++) - hybbx_chess_send_board(game, game->spectators[j]); - } - - if (game->result != CHESS_RESULT_NONE) { - const char *res; - unsigned j; - switch (game->result) { - case CHESS_WHITE_WINS: res = "White wins!"; break; - case CHESS_BLACK_WINS: res = "Black wins!"; break; - case CHESS_DRAW_STALEMATE: res = "Stalemate!"; break; - case CHESS_DRAW_AGREEMENT: res = "Draw!"; break; - default: res = "Game over."; break; - } - snprintf(line, sizeof(line), "[%s] %s", game->game_id, res); - if (game->white_session) - hybbx_session_write_line(game->white_session, line); - if (game->black_session) - hybbx_session_write_line(game->black_session, line); - for (j = 0; j < game->spectator_count; j++) - hybbx_session_write_line(game->spectators[j], line); - } - - return HYBBX_OK; - } - - /* ── /chess resign ─────────────────────────────────────── */ - if (str_ieq(sub, "resign")) { - unsigned i; - game = NULL; - for (i = 0; i < hybbx_chess_max_games(); i++) { - chess_game_t *g = hybbx_chess_get_game(i); - if (g && g->active && hybbx_chess_is_player(g, session)) { - game = g; - break; - } - } - if (game == NULL) { - hybbx_session_write_line(session, "You are not in a game."); - return HYBBX_ERR_NOT_FOUND; - } - hybbx_chess_resign(game, session); - snprintf(line, sizeof(line), "[%s] %s resigned.", - game->game_id, hybbx_session_username(session)); - if (game->white_session) - hybbx_session_write_line(game->white_session, line); - if (game->black_session) - hybbx_session_write_line(game->black_session, line); - { - unsigned j; - for (j = 0; j < game->spectator_count; j++) - hybbx_session_write_line(game->spectators[j], line); - } - return HYBBX_OK; - } - - /* ── /chess draw ───────────────────────────────────────── */ - if (str_ieq(sub, "draw")) { - unsigned i; - game = NULL; - for (i = 0; i < hybbx_chess_max_games(); i++) { - chess_game_t *g = hybbx_chess_get_game(i); - if (g && g->active && hybbx_chess_is_player(g, session)) { - game = g; - break; - } - } - if (game == NULL) { - hybbx_session_write_line(session, "You are not in a game."); - return HYBBX_ERR_NOT_FOUND; - } - hybbx_chess_offer_draw(game, session); - snprintf(line, sizeof(line), "[%s] Draw agreed.", - game->game_id); - if (game->white_session) - hybbx_session_write_line(game->white_session, line); - if (game->black_session) - hybbx_session_write_line(game->black_session, line); - { - unsigned j; - for (j = 0; j < game->spectator_count; j++) - hybbx_session_write_line(game->spectators[j], line); - } - return HYBBX_OK; - } - - hybbx_session_write_line(session, - "Unknown chess command. /chess help for list."); - return HYBBX_ERR_NOT_FOUND; -} diff --git a/src/core/command.c b/src/core/command.c index 8cd4e5c..3614821 100644 --- a/src/core/command.c +++ b/src/core/command.c @@ -18,7 +18,6 @@ #include "hybbx/password.h" #include "hybbx/traffic.h" #include "hybbx/monitor.h" -#include "hybbx/chess.h" #include "hybbx/util.h" #include <stdio.h> @@ -2304,6 +2303,11 @@ hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service, return cmd_rules(service, session); } + /* Plugin command intercept — try all plugins first */ + if (hybbx_service_try_plugin_command(service, session, cmd) == HYBBX_OK) { + return HYBBX_OK; + } + if (str_ieq(cmd->verb, "who") || str_ieq(cmd->verb, "online")) { return cmd_who(service, session); } @@ -2312,9 +2316,6 @@ hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service, return cmd_monitor(service, session, cmd); } - if (str_ieq(cmd->verb, "chess") || str_ieq(cmd->verb, "play")) { - return cmd_chess(service, session, cmd); - } if (str_ieq(cmd->verb, "users")) { diff --git a/src/core/service.c b/src/core/service.c index 6cf2b16..20934a7 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -7,6 +7,7 @@ #include "storage_private.h" #include "hybbx/registry.h" #include "hybbx/config.h" +#include "hybbx/command.h" #include "hybbx/crypto_config.h" #include "hybbx/traffic.h" #include "hybbx/circuit_tcp.h" @@ -26,7 +27,6 @@ void hybbx_mains_proxy_plugin_tick(void); #include "hybbx/instance.h" #include "hybbx/log.h" #include "hybbx/monitor.h" -#include "hybbx/chess.h" #include <errno.h> #include "hybbx/security.h" #include "hybbx/security_ban.h" @@ -168,7 +168,6 @@ void hybbx_service_destroy(hybbx_service_t *service) } hybbx_monitor_shutdown(); - hybbx_chess_init(); hybbx_log_shutdown(); hybbx_security_log_shutdown(); hybbx_security_ban_shutdown(); @@ -989,13 +988,22 @@ hybbx_result_t hybbx_service_run(hybbx_service_t *service) health_tick++; if (health_tick >= 300u) { health_tick = 0; - hybbx_log_info("[service] health: running=%d transports=%u sessions_active (tick)", + hybbx_log_info("[service] health: running=%d transports=%zu sessions_active (tick)", svc->running, svc->transport_count); } hybbx_security_ban_tick(); hybbx_monitor_tick(service); - hybbx_chess_tick(service); + /* Plugin ticks — iterate all registered plugins */ + { + size_t pi; + for (pi = 0; pi < svc->transport_count; pi++) { + const hybbx_transport_plugin_t *p = svc->transports[pi].plugin; + if (p != NULL && p->tick != NULL) { + p->tick(service); + } + } + } hybbx_broadcast_ax25_tick(service); #ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY hybbx_mains_proxy_plugin_tick(); @@ -1016,6 +1024,31 @@ hybbx_result_t hybbx_service_run(hybbx_service_t *service) return HYBBX_OK; } +hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + size_t i; + + if (svc == NULL || cmd == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + for (i = 0; i < svc->transport_count; i++) { + const hybbx_transport_plugin_t *p = svc->transports[i].plugin; + if (p != NULL && p->on_command != NULL) { + hybbx_result_t rc = p->on_command(service, session, cmd); + if (rc == HYBBX_OK) { + return HYBBX_OK; + } + } + } + + return HYBBX_ERR_NOT_FOUND; +} + void hybbx_service_stop(hybbx_service_t *service) { struct hybbx_service_internal *svc = @@ -1223,7 +1256,6 @@ hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service, hybbx_log_config_apply(config); hybbx_security_log_config_apply(config); hybbx_monitor_config_apply(config); - hybbx_chess_config_apply(config); hybbx_security_ban_config_apply(config); service_apply_texts(svc, config); hybbx_chat_config_apply(&svc->chat, config); @@ -45,6 +45,9 @@ extern const hybbx_transport_plugin_t hybbx_plugin_ssh; #ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET extern const hybbx_transport_plugin_t hybbx_plugin_websocket; #endif +#ifdef HYBBX_HAVE_PLUGIN_ENTERTAIN +extern const hybbx_transport_plugin_t hybbx_plugin_entertain; +#endif #ifdef HYBBX_HAVE_PLUGIN_BAYCOM extern const hybbx_transport_plugin_t hybbx_plugin_baycom; #endif @@ -73,6 +76,9 @@ static void register_builtin_plugins(void) #ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET hybbx_registry_register(&hybbx_plugin_websocket); #endif +#ifdef HYBBX_HAVE_PLUGIN_ENTERTAIN + hybbx_registry_register(&hybbx_plugin_entertain); +#endif #ifdef HYBBX_HAVE_PLUGIN_BAYCOM hybbx_registry_register(&hybbx_plugin_baycom); #endif diff --git a/text/news.txt b/text/news.txt index 9823220..6fbe11f 100644 --- a/text/news.txt +++ b/text/news.txt @@ -1 +1 @@ -HyBBX 2.4.1 — Menu/index: General command list wraps at 80 columns (/session /version visible). BayCom: optional transport plugin (CMake ON; example INI `baycom=no`). MAX25 attach: `kiss_entry=none`, `[max25] check=yes`. Includes v2.0.0-p2 baseline (menu registry, TNC recovery, broadcast fixes). +HyBBX 2.8.0 — Menu/index: General command list wraps at 80 columns (/session /version visible). BayCom: optional transport plugin (CMake ON; example INI `baycom=no`). MAX25 attach: `kiss_entry=none`, `[max25] check=yes`. Includes v2.0.0-p2 baseline (menu registry, TNC recovery, broadcast fixes). diff --git a/text/rules.txt b/text/rules.txt index 837efbc..12a4a99 100644 --- a/text/rules.txt +++ b/text/rules.txt @@ -1,4 +1,4 @@ -HyBBX — Terms of use (v2.4.0) +HyBBX — Terms of use (v2.8.0) HyBBX is a session service for low-bandwidth links: mail, chat, conference, and commands over telnet, SSH, or WebSocket. RF paths |
