diff options
Diffstat (limited to 'plugins/entertain/chess_cmd.c')
| -rw-r--r-- | plugins/entertain/chess_cmd.c | 500 |
1 files changed, 349 insertions, 151 deletions
diff --git a/plugins/entertain/chess_cmd.c b/plugins/entertain/chess_cmd.c index 3e57223..fdfcfed 100644 --- a/plugins/entertain/chess_cmd.c +++ b/plugins/entertain/chess_cmd.c @@ -1,39 +1,201 @@ /* * Entertain plugin — chess_cmd.c - * /chess command handler (join/leave/list/move/resign/draw/clear/status/board). + * /chess · /play · /mv command handler. */ #include "entertain_chess.h" #include "hybbx/session.h" #include "hybbx/service.h" #include "hybbx/command.h" +#include <ctype.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++; + if (a == NULL || b == NULL) { + return 0; + } + while (*a != '\0' && *b != '\0') { + char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? (char)(*b + 32) : *b; + + if (ca != cb) { + return 0; + } + a++; + b++; } return *a == *b; } +static void chess_help(hybbx_session_t *session) +{ + char line[96]; + + hybbx_session_write_line(session, "Chess (Entertain area):"); + hybbx_session_write_line(session, " /chess new Create game (you = White)"); + hybbx_session_write_line(session, " /chess join [id] Join as Black"); + hybbx_session_write_line(session, " /chess watch [id] Spectate"); + hybbx_session_write_line(session, " /chess board Show your board"); + hybbx_session_write_line(session, " /chess move e2 e4 Move (or e2e4)"); + hybbx_session_write_line(session, " /mv e2 e4 Move alias"); + hybbx_session_write_line(session, " /chess move e7 e8 q Promote (q/r/b/n)"); + hybbx_session_write_line(session, " /chess resign Resign"); + hybbx_session_write_line(session, " /chess draw Offer / accept draw"); + hybbx_session_write_line(session, " /chess list List games"); + hybbx_session_write_line(session, " /chess leave Stop spectating"); + hybbx_session_write_line(session, " /chess clear Free finished game slots"); + hybbx_session_write_line(session, " /play … Alias for /chess"); + snprintf(line, sizeof(line), + "Notes: no castling/en passant. Spectator refresh %us.", + CHESS_REFRESH_SEC); + hybbx_session_write_line(session, line); +} + +static void announce_result(chess_game_t *g) +{ + char line[128]; + const char *res; + unsigned j; + + if (g == NULL || g->result == CHESS_RESULT_NONE) { + return; + } + switch (g->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 agreed"; + break; + default: + res = "Game over"; + break; + } + snprintf(line, sizeof(line), "[%s] %s", g->game_id, res); + if (g->white_session != NULL) { + hybbx_session_write_line(g->white_session, line); + } + if (g->black_session != NULL) { + hybbx_session_write_line(g->black_session, line); + } + for (j = 0; j < g->spectator_count; j++) { + hybbx_session_write_line(g->spectators[j], line); + } +} + +static int parse_move_args(const hybbx_parsed_command_t *cmd, unsigned start, + char *from, size_t from_len, char *to, size_t to_len, + char *promo) +{ + const char *a; + const char *b; + + *promo = 0; + if (cmd == NULL || from == NULL || to == NULL || from_len < 3 || to_len < 3) { + return 0; + } + + if (cmd->argc < start + 1) { + return 0; + } + a = cmd->argv[start]; + if (a == NULL) { + return 0; + } + + /* Compact: e2e4 or e7e8q */ + if (strlen(a) >= 4 && cmd->argc == start + 1) { + if (!isalpha((unsigned char)a[0]) || !isdigit((unsigned char)a[1]) || + !isalpha((unsigned char)a[2]) || !isdigit((unsigned char)a[3])) { + return 0; + } + from[0] = (char)tolower((unsigned char)a[0]); + from[1] = a[1]; + from[2] = '\0'; + to[0] = (char)tolower((unsigned char)a[2]); + to[1] = a[3]; + to[2] = '\0'; + if (a[4] != '\0') { + *promo = a[4]; + } + return 1; + } + + if (cmd->argc < start + 2) { + return 0; + } + b = cmd->argv[start + 1]; + if (b == NULL) { + return 0; + } + snprintf(from, from_len, "%s", a); + snprintf(to, to_len, "%s", b); + if (cmd->argc >= start + 3 && cmd->argv[start + 2] != NULL && + cmd->argv[start + 2][0] != '\0') { + *promo = cmd->argv[start + 2][0]; + } + return 1; +} + +static hybbx_result_t do_move(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd, + unsigned arg_start) +{ + chess_game_t *game; + char from[8]; + char to[8]; + char promo = 0; + char err[64]; + char line[96]; + + if (!parse_move_args(cmd, arg_start, from, sizeof(from), to, sizeof(to), + &promo)) { + hybbx_session_write_line(session, + "Usage: /chess move e2 e4 | /mv e2 e4 | /mv e2e4"); + return HYBBX_ERR_INVALID; + } + + game = chess_find_player_game(session); + if (game == NULL || game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "No active game. /chess new or /chess join"); + return HYBBX_ERR_DENIED; + } + if (chess_player_color(game, session) != game->turn) { + hybbx_session_write_line(session, "Not your turn."); + return HYBBX_ERR_DENIED; + } + if (!chess_make_move(game, from, to, promo, err, sizeof(err))) { + snprintf(line, sizeof(line), "Illegal: %s", err); + hybbx_session_write_line(session, line); + return HYBBX_ERR_INVALID; + } + chess_broadcast_board(game); + if (game->result != CHESS_RESULT_NONE) { + announce_result(game); + } + return HYBBX_OK; +} + 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]; + char line[160]; + int mv_shortcut = 0; + (void)service; if (!hybbx_session_logged_in(session)) { @@ -41,134 +203,168 @@ hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, return HYBBX_ERR_DENIED; } - sub = (cmd->argc >= 1) ? cmd->argv[0] : "help"; + /* /mv … → treat as move with args at argv[0..] */ + if (cmd->verb != NULL && ieq(cmd->verb, "mv")) { + mv_shortcut = 1; + sub = "move"; + } else { + 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); + chess_help(session); return HYBBX_OK; } - /* ── status ────────────────────────────────────────────── */ if (ieq(sub, "status")) { - snprintf(line, sizeof(line), "Chess: %u max games", chess_max_games()); + snprintf(line, sizeof(line), "Chess: max %u concurrent games", + chess_max_games()); hybbx_session_write_line(session, line); return HYBBX_OK; } - /* ── list ──────────────────────────────────────────────── */ if (ieq(sub, "list") || ieq(sub, "ls")) { unsigned found = 0; + unsigned i; + hybbx_session_write_line(session, "Chess games:"); - for (unsigned i = 0; i < chess_max_games(); i++) { + for (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"; + const char *st; + + if (g == NULL) { + continue; + } + if (g->result != CHESS_RESULT_NONE) { + st = "finished"; + } else if (g->black_session == NULL) { + st = "waiting"; + } else { + st = "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."); + 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; + + if (chess_find_player_game(session) != NULL) { + hybbx_session_write_line(session, + "Already in a game. /chess resign or wait for finish + /chess clear"); + 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); + g = chess_create_game(session, hybbx_session_username(session)); + if (g == NULL) { + hybbx_session_write_line(session, + "No free game slots. /chess clear to free finished games."); + 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..."); + hybbx_session_write_line(session, "Waiting for Black: /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; - } + const char *id = (!mv_shortcut && cmd->argc >= 2) ? cmd->argv[1] : NULL; + chess_game_t *g; + + if (chess_find_player_game(session) != NULL) { + hybbx_session_write_line(session, "Already in a game."); + return HYBBX_ERR_DENIED; } - chess_game_t *g = NULL; - if (id) { + if (id != NULL) { 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; } + if (g == NULL || !g->active) { + hybbx_session_write_line(session, "Game not found."); + return HYBBX_ERR_NOT_FOUND; + } + if (g->black_session != NULL) { + hybbx_session_write_line(session, "Game is full."); + return HYBBX_ERR_DENIED; + } + if (g->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + 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; } + if (g == NULL) { + hybbx_session_write_line(session, "No open games. /chess new"); + return HYBBX_ERR_NOT_FOUND; + } + } + if (!chess_join_game(g, session, hybbx_session_username(session))) { + hybbx_session_write_line(session, "Cannot join that game."); + return HYBBX_ERR_DENIED; } - chess_join_game(g, session, hybbx_session_username(session)); - snprintf(line, sizeof(line), "Joined %s as Black. Game on!", g->game_id); + snprintf(line, sizeof(line), "Joined %s as Black. White to move.", + g->game_id); hybbx_session_write_line(session, line); - chess_send_board(g, g->white_session); - chess_send_board(g, session); + if (g->white_session != NULL) { + snprintf(line, sizeof(line), "[%s] %s joined as Black.", + g->game_id, g->black_name); + hybbx_session_write_line(g->white_session, line); + } + chess_broadcast_board(g); return HYBBX_OK; } - /* ── watch ─────────────────────────────────────────────── */ if (ieq(sub, "watch") || ieq(sub, "spectate")) { - const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + const char *id = (!mv_shortcut && cmd->argc >= 2) ? cmd->argv[1] : NULL; chess_game_t *g = NULL; - if (id) { + + if (id != NULL) { g = chess_find_game_by_id(id); } else { - for (unsigned i = 0; i < chess_max_games(); i++) { + unsigned i; + + for (i = 0; i < chess_max_games(); i++) { chess_game_t *t = chess_get_game(i); - if (t && t->black_session) { g = t; break; } + + if (t != NULL && t->black_session != NULL && + t->result == CHESS_RESULT_NONE) { + 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); + if (g == NULL || !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, + "Cannot watch (player or spectator list full)."); + return HYBBX_ERR_DENIED; + } + snprintf(line, sizeof(line), + "Watching %s. Board refreshes every %us for spectators.", + 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++) { + unsigned i; + + for (i = 0; i < chess_max_games(); i++) { chess_game_t *g = chess_get_game(i); - if (g && chess_is_spectator(g, session)) { + + if (g != NULL && chess_is_spectator(g, session)) { chess_remove_spectator(g, session); hybbx_session_write_line(session, "Stopped watching."); return HYBBX_OK; @@ -178,97 +374,99 @@ hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, return HYBBX_ERR_NOT_FOUND; } - /* ── board ─────────────────────────────────────────────── */ if (ieq(sub, "board") || ieq(sub, "show")) { - for (unsigned i = 0; i < chess_max_games(); i++) { + unsigned i; + + for (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))) { + + if (g != NULL && + (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."); + 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; + if (ieq(sub, "move") || ieq(sub, "m")) { + return do_move(session, cmd, mv_shortcut ? 0u : 1u); } - /* ── 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; } + chess_game_t *game = chess_find_player_game(session); + const char *who; + + if (game == NULL) { + hybbx_session_write_line(session, "Not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + if (game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + return HYBBX_ERR_DENIED; } - if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; } + who = hybbx_session_username(session); 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); + snprintf(line, sizeof(line), "[%s] %s resigned.", game->game_id, + who != NULL ? who : "?"); + if (game->white_session != NULL) { + hybbx_session_write_line(game->white_session, line); + } + if (game->black_session != NULL) { + 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); + } + } + announce_result(game); 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; } + chess_game_t *game = chess_find_player_game(session); + struct hybbx_session *other; + int agreed; + + if (game == NULL) { + hybbx_session_write_line(session, "Not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + if (game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + return HYBBX_ERR_DENIED; + } + agreed = chess_offer_draw(game, session); + if (agreed) { + announce_result(game); + chess_broadcast_board(game); + return HYBBX_OK; + } + hybbx_session_write_line(session, "Draw offered. Opponent: /chess draw"); + other = (game->white_session == session) ? game->black_session + : game->white_session; + if (other != NULL) { + snprintf(line, sizeof(line), + "[%s] Draw offered by %s — /chess draw to accept", + game->game_id, + hybbx_session_username(session) + ? hybbx_session_username(session) + : "?"); + hybbx_session_write_line(other, line); } - 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."); + unsigned n = chess_clear_finished(); + + snprintf(line, sizeof(line), "Cleared %u finished game(s).", n); + hybbx_session_write_line(session, line); return HYBBX_OK; } |
