summaryrefslogtreecommitdiff
path: root/plugins/entertain/chess.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-09 11:54:17 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-09 11:54:17 +0000
commita989bd16c40e04bba5a5404b0ef0cd22f19d2707 (patch)
tree604a0439c7d8c022cbd483d62bdc2cfc1c8d18f8 /plugins/entertain/chess.c
parenta72e5ea7558e5172c08d397d6f1e0e0f9273e694 (diff)
now including entertain/chess
Diffstat (limited to 'plugins/entertain/chess.c')
-rw-r--r--plugins/entertain/chess.c552
1 files changed, 552 insertions, 0 deletions
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]);
+ }
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com