summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-09 12:22:53 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-09 12:22:53 +0000
commite1730d8012fca0a712de588285342f7ad10dd804 (patch)
treeb351ae5da04fe10a211be772636b2bf8de6d49dd
parenta989bd16c40e04bba5a5404b0ef0cd22f19d2707 (diff)
now including entertain/chess
-rw-r--r--docs/ENTERTAIN.md43
-rw-r--r--docs/MASTER-GUIDE.md2
-rw-r--r--include/hybbx/networks.h2
-rw-r--r--plugins/entertain/chess.c801
-rw-r--r--plugins/entertain/chess_cmd.c500
-rw-r--r--plugins/entertain/entertain.c40
-rw-r--r--plugins/entertain/entertain_chess.h26
-rw-r--r--share/commands.yaml10
-rw-r--r--share/hybbx-main.ini.example2
-rw-r--r--share/hybbx-standalone.ini.example2
-rw-r--r--share/hybbxd-main.ini.example2
-rw-r--r--src/core/command.c19
-rw-r--r--src/core/instance.c23
-rw-r--r--src/core/networks.c15
-rw-r--r--src/core/service.c12
15 files changed, 1062 insertions, 437 deletions
diff --git a/docs/ENTERTAIN.md b/docs/ENTERTAIN.md
index cbdec78..f6b9667 100644
--- a/docs/ENTERTAIN.md
+++ b/docs/ENTERTAIN.md
@@ -1,21 +1,37 @@
-# Entertain Area · HyBBX 2.8.0
+# Entertain Area · HyBBX
-Optional Main-only plugins for games and apps — not on Secondary RF hosts.
+Optional **Main / standalone Main** feature plugin for games (chess first).
+Not loaded on Secondary or Proxy. AmigaOS builds force the plugin **OFF** at CMake.
-## Plugin matrix
+## Enable
-| Plugin | Status | Notes |
-|--------|--------|-------|
-| Entertain Area framework | opt-in CMake | Main hosts only |
-| Chess / games | deferred | future or doc removal |
+| Layer | Setting |
+|-------|---------|
+| Build | `-DHYBBX_PLUGIN_ENTERTAIN=ON` (default on POSIX; forced off on AmigaOS) |
+| INI | `[networks] entertain=yes` |
+| Role | `hybbxd` Main (or standalone Main) |
-## Config matrix
+```ini
+[networks]
+entertain = yes
+```
-| Item | Value |
-|------|-------|
-| Enable | `-DHYBBX_PLUGIN_ENTERTAIN=ON` (when available) |
-| Host role | Main only — Secondary runs RF transports |
-| User access | Level-gated via `[areas]` / `share/areas.yaml` |
+Log line on success: `[entertain] plugin loaded — chess_max_games=…`
+
+## Chess
+
+| Command | Action |
+|---------|--------|
+| `/chess help` | Command list |
+| `/chess new` | Create game (White) |
+| `/chess join [id]` | Join as Black |
+| `/chess watch [id]` | Spectate |
+| `/chess move e2 e4` | Move (`/mv e2 e4` or `/mv e2e4`) |
+| `/chess resign` / `/chess draw` | Resign / offer-accept draw |
+| `/chess list` / `/chess board` / `/chess clear` | List / show / free finished slots |
+| `/play …` | Alias for `/chess` |
+
+Limits: no castling, no en passant; pawn promotes to Q/R/B/N. ASCII board for telnet/SSH.
## Related
@@ -23,3 +39,4 @@ Optional Main-only plugins for games and apps — not on Secondary RF hosts.
|------|-----|
| Topology | [TOPOLOGY.md](TOPOLOGY.md) |
| Commands | [COMMANDS.md](COMMANDS.md) |
+| Areas | `share/areas.yaml` · Entertain menu (User+) |
diff --git a/docs/MASTER-GUIDE.md b/docs/MASTER-GUIDE.md
index 53895af..b6d025d 100644
--- a/docs/MASTER-GUIDE.md
+++ b/docs/MASTER-GUIDE.md
@@ -61,7 +61,7 @@ Full key tables: frozen `docs/MANUAL.md`. Vault INI copy (reference station): `s
|---------|---------|
| `[service]` | Name, session limit, prompt |
| `[storage]` | `flatfile` or `sqlite`; user shards under `users/` |
-| `[networks]` | Enable plugins: `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket` |
+| `[networks]` | Enable plugins: `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket`, `entertain` (Main chess) |
| `[transport.telnet]` | Bind, port `2323` |
| `[transport.circuit]` | HBX hub port `7323`, `max_links` (default 8, max 16) |
| `[transport.packet_radioN]` | TNC instance — device, `tnc=` profile, `protocol=kiss` |
diff --git a/include/hybbx/networks.h b/include/hybbx/networks.h
index ab8e954..b58642d 100644
--- a/include/hybbx/networks.h
+++ b/include/hybbx/networks.h
@@ -35,6 +35,8 @@ typedef struct hybbx_networks_config {
int circuit;
/** Main-to-Main mesh (`transport.mains_proxy`) — Secondary primary. */
int mains_proxy;
+ /** Entertain feature plugin (chess, …) — Main / standalone Main only. */
+ int entertain;
} hybbx_networks_config_t;
void hybbx_networks_config_defaults(hybbx_networks_config_t *networks);
diff --git a/plugins/entertain/chess.c b/plugins/entertain/chess.c
index 7875d3b..bbf077b 100644
--- a/plugins/entertain/chess.c
+++ b/plugins/entertain/chess.c
@@ -1,6 +1,6 @@
/*
* Entertain plugin — chess.c
- * Chess game engine: board, moves, check/checkmate, SAN, tick broadcast.
+ * Board, move legality (no castling / en passant), checkmate, SAN, tick.
*/
#include "entertain_chess.h"
#include "hybbx/session.h"
@@ -15,39 +15,13 @@
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];
@@ -55,9 +29,11 @@ static void board_init(chess_board_t *b)
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++)
+ for (unsigned r = 2; r < 6; r++) {
+ for (f = 0; f < 8; f++) {
b->sq[r][f] = CHESS_EMPTY;
+ }
+ }
}
/* ── Lifecycle ─────────────────────────────────────────────── */
@@ -65,118 +41,194 @@ static void board_init(chess_board_t *b)
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)
+ 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;
+ for (i = 0; i < CHESS_GAME_MAX; i++) {
snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), "G%u", i + 1);
+ g_games[i].active = 0;
}
}
void chess_shutdown(void)
{
unsigned i;
- for (i = 0; i < CHESS_GAME_MAX; i++) g_games[i].active = 0;
+
+ for (i = 0; i < CHESS_GAME_MAX; i++) {
+ g_games[i].active = 0;
+ }
}
-unsigned chess_max_games(void) { return g_max_games; }
+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')
+ char c0;
+ char c1;
+
+ if (s == NULL || s[0] == '\0' || s[1] == '\0') {
+ return 0;
+ }
+ c0 = (char)tolower((unsigned char)s[0]);
+ c1 = s[1];
+ if (c0 < 'a' || c0 > 'h' || c1 < '1' || c1 > '8') {
return 0;
- *f = (unsigned)(s[0] - 'a');
- *r = (unsigned)(s[1] - '1');
+ }
+ *f = (unsigned)(c0 - 'a');
+ *r = (unsigned)(c1 - '1');
return 1;
}
static chess_color_t pcol(char p)
{
- if (p == CHESS_EMPTY) return CHESS_COLOR_NONE;
+ 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; }
+static char pup(char p)
+{
+ return (char)toupper((unsigned char)p);
+}
+
+static int inb(unsigned r, unsigned f)
+{
+ return r < 8 && f < 8;
+}
+
+static const char *color_name(chess_color_t c)
+{
+ if (c == CHESS_WHITE) {
+ return "White";
+ }
+ if (c == CHESS_BLACK) {
+ return "Black";
+ }
+ return "?";
+}
/* ── 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'; }
+ unsigned r;
+ unsigned f;
+ char P;
+ char N;
+ char B;
+ char R;
+ char Q;
+ char 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;
+ 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';
+ }
+
+ 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;
+ 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;
+ static const int kr[] = {2, 2, -2, -2, 1, 1, -1, -1};
+ static const int kf[] = {1, -1, 1, -1, 2, -2, 2, -2};
+ unsigned i;
+
+ for (i = 0; i < 8; i++) {
+ int nr = (int)rk + kr[i];
+ int nf = (int)fl + kf[i];
+
+ if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8 &&
+ 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) {
+ static const int dr[] = {1, 1, -1, -1};
+ static const int df[] = {1, -1, 1, -1};
+ unsigned d;
+
+ for (d = 0; d < 4; d++) {
+ r = rk;
+ f = fl;
+ for (;;) {
r = (unsigned)((int)r + dr[d]);
f = (unsigned)((int)f + df[d]);
- if (!inb(r, f)) break;
+ 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;
+ 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) {
+ static const int dr[] = {1, -1, 0, 0};
+ static const int df[] = {0, 0, 1, -1};
+ unsigned d;
+
+ for (d = 0; d < 4; d++) {
+ r = rk;
+ f = fl;
+ for (;;) {
r = (unsigned)((int)r + dr[d]);
f = (unsigned)((int)f + df[d]);
- if (!inb(r, f)) break;
+ 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;
+ 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;
+ static const int dr[] = {1, 1, 1, 0, 0, -1, -1, -1};
+ static const int df[] = {1, 0, -1, 1, -1, 1, 0, -1};
+ unsigned d;
+
+ for (d = 0; d < 8; d++) {
+ int nr = (int)rk + dr[d];
+ int nf = (int)fl + df[d];
+
+ if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8 &&
+ b->sq[nr][nf] == K) {
+ return 1;
+ }
}
}
@@ -187,17 +239,30 @@ 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; }
+ unsigned r;
+ unsigned f;
+
+ for (r = 0; r < 8; r++) {
+ for (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;
+ unsigned kr;
+ unsigned kf;
chess_color_t opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE;
- if (!find_king(b, side, &kr, &kf)) return 0;
+
+ if (!find_king(b, side, &kr, &kf)) {
+ return 0;
+ }
return sq_attacked(b, kr, kf, opp);
}
@@ -208,55 +273,93 @@ static int pseudo_pawn(const chess_board_t *b, unsigned fr, unsigned ff,
{
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;
+ 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 &&
- b->sq[fr+(unsigned)dir][ff] == CHESS_EMPTY &&
- b->sq[tr][tf] == CHESS_EMPTY) return 1;
+ 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)
+ 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);
+ 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 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);
+ int dr = 0;
+ int df = 0;
+ unsigned r;
+ unsigned 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);
+ 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;
+ int dr = abs((int)tr - (int)fr);
+ int 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);
+ 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);
+ default:
+ break;
}
return 0;
}
@@ -265,25 +368,42 @@ 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;
+
+ 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)))
+ ((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;
+ unsigned fr;
+ unsigned ff;
+ unsigned tr;
+ unsigned tf;
+
+ for (fr = 0; fr < 8; fr++) {
+ for (ff = 0; ff < 8; ff++) {
+ if (pcol(b->sq[fr][ff]) != side) {
+ continue;
+ }
+ for (tr = 0; tr < 8; tr++) {
+ for (tf = 0; tf < 8; tf++) {
+ if (is_legal(b, fr, ff, tr, tf, side)) {
+ return 1;
+ }
+ }
+ }
}
+ }
return 0;
}
@@ -294,77 +414,182 @@ static void make_san(char *out, size_t len, char pc, unsigned ff,
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));
+ 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));
+ n = snprintf(out, len, "%c%c%d", pup(pc), 'a' + (int)tf,
+ (int)(tr + 1));
+ }
+ if (n < 0) {
+ n = 0;
+ }
+ 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, "+");
}
- 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;
+ if (i >= g_max_games || !g_games[i].active) {
+ return NULL;
+ }
+ return &g_games[i];
+}
+
+void chess_release_game(chess_game_t *g)
+{
+ char id[8];
+
+ if (g == NULL) {
+ return;
+ }
+ hybbx_strlcpy(id, g->game_id, sizeof(id));
+ memset(g, 0, sizeof(*g));
+ hybbx_strlcpy(g->game_id, id, sizeof(g->game_id));
+ g->active = 0;
+}
+
+unsigned chess_clear_finished(void)
+{
+ unsigned i;
+ unsigned n = 0;
+
+ for (i = 0; i < g_max_games; i++) {
+ if (g_games[i].active && g_games[i].result != CHESS_RESULT_NONE) {
+ chess_release_game(&g_games[i]);
+ n++;
+ }
+ }
+ return n;
}
chess_game_t *chess_create_game(struct hybbx_session *white, const char *name)
{
unsigned i;
+ chess_game_t *slot = NULL;
+
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];
+ slot = &g_games[i];
+ break;
+ }
+ if (g_games[i].result != CHESS_RESULT_NONE) {
+ slot = &g_games[i];
+ break;
}
}
- return NULL;
+ if (slot == NULL) {
+ return NULL;
+ }
+
+ {
+ char id[8];
+
+ hybbx_strlcpy(id, slot->game_id, sizeof(id));
+ memset(slot, 0, sizeof(*slot));
+ hybbx_strlcpy(slot->game_id, id, sizeof(slot->game_id));
+ }
+ slot->active = 1;
+ hybbx_strlcpy(slot->white_name, name != NULL ? name : "?", CHESS_NAME_MAX);
+ slot->white_session = white;
+ board_init(&slot->board);
+ slot->turn = CHESS_WHITE;
+ slot->move_number = 1;
+ hybbx_log_info("[chess] game %s created by %s", slot->game_id,
+ slot->white_name);
+ return slot;
}
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];
+ unsigned i;
+
+ for (i = 0; i < g_max_games; i++) {
+ if (g_games[i].active && g_games[i].black_session == NULL &&
+ g_games[i].result == CHESS_RESULT_NONE) {
+ 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)
+ 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;
+}
+
+chess_game_t *chess_find_player_game(const struct hybbx_session *s)
+{
+ unsigned i;
+
+ if (s == NULL) {
+ return NULL;
+ }
+ for (i = 0; i < g_max_games; i++) {
+ if (g_games[i].active && chess_is_player(&g_games[i], s)) {
return &g_games[i];
+ }
+ }
return NULL;
}
-int chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name)
+int chess_join_game(chess_game_t *g, struct hybbx_session *black,
+ const char *name)
{
- if (!g || g->black_session) return 0;
+ if (g == NULL || black == NULL || g->black_session != NULL ||
+ g->result != CHESS_RESULT_NONE) {
+ return 0;
+ }
+ if (g->white_session == black) {
+ 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);
+ hybbx_strlcpy(g->black_name, name != NULL ? name : "?", CHESS_NAME_MAX);
+ hybbx_log_info("[chess] %s joined %s as black", g->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;
+
+ if (g == NULL || spec == NULL) {
+ 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;
}
@@ -372,7 +597,10 @@ int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec)
void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec)
{
unsigned i;
- if (!g || !spec) return;
+
+ if (g == NULL || spec == NULL) {
+ return;
+ }
for (i = 0; i < g->spectator_count; i++) {
if (g->spectators[i] == spec) {
g->spectators[i] = g->spectators[--g->spectator_count];
@@ -383,23 +611,37 @@ 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)
{
- return g && s && (g->white_session == s || g->black_session == s);
+ return g != NULL && s != NULL &&
+ (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;
+
+ if (g == NULL || s == NULL) {
+ 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)
+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;
+ if (g == NULL || s == NULL) {
+ return CHESS_COLOR_NONE;
+ }
+ if (g->white_session == s) {
+ return CHESS_WHITE;
+ }
+ if (g->black_session == s) {
+ return CHESS_BLACK;
+ }
return CHESS_COLOR_NONE;
}
@@ -408,32 +650,74 @@ chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_sessi
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;
+ unsigned fr;
+ unsigned ff;
+ unsigned tr;
+ unsigned tf;
+ char piece;
+ char captured;
+ int check;
+ int mate;
+ chess_color_t side;
+ chess_color_t opp;
+ char promo_piece = 0;
- if (!g || g->result != CHESS_RESULT_NONE) {
- if (err) snprintf(err, errlen, "Game is over.");
+ if (g == NULL || g->result != CHESS_RESULT_NONE) {
+ if (err != NULL) {
+ snprintf(err, errlen, "Game is over");
+ }
return 0;
}
+ if (g->black_session == NULL) {
+ if (err != NULL) {
+ snprintf(err, errlen, "Waiting for Black");
+ }
+ 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; }
+ if (!parse_sq(from, &fr, &ff)) {
+ if (err != NULL) {
+ snprintf(err, errlen, "Bad square '%s'", from != NULL ? from : "");
+ }
+ return 0;
+ }
+ if (!parse_sq(to, &tr, &tf)) {
+ if (err != NULL) {
+ snprintf(err, errlen, "Bad square '%s'", to != NULL ? 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; }
+ if (pcol(piece) != side) {
+ if (err != NULL) {
+ snprintf(err, errlen, "Not your piece");
+ }
+ return 0;
+ }
+ if (!is_legal(&g->board, fr, ff, tr, tf, side)) {
+ if (err != NULL) {
+ 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))) {
+ ((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);
+
+ if (pp != 'Q' && pp != 'R' && pp != 'B' && pp != 'N') {
+ pp = 'Q';
+ }
+ promo_piece = pp;
+ g->board.sq[tr][tf] =
+ (side == CHESS_WHITE) ? pp : (char)tolower((unsigned char)pp);
}
opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE;
@@ -441,14 +725,20 @@ int chess_make_move(chess_game_t *g, const char *from, const char *to,
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);
+ captured != CHESS_EMPTY, check, mate, promo_piece);
- 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;
+ 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++;
+ if (opp == CHESS_WHITE) {
+ g->move_number++;
+ }
g->tick_counter = 0;
+ g->draw_offered_by = CHESS_COLOR_NONE;
hybbx_log_info("[chess] %s: %s %s", g->game_id,
(side == CHESS_WHITE) ? g->white_name : g->black_name,
@@ -460,16 +750,36 @@ int chess_make_move(chess_game_t *g, const char *from, const char *to,
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;
+ if (g == NULL || 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;
+ }
+ g->draw_offered_by = CHESS_COLOR_NONE;
}
-void chess_offer_draw(chess_game_t *g, struct hybbx_session *who)
+int 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;
+ chess_color_t me;
+
+ if (g == NULL || who == NULL || g->result != CHESS_RESULT_NONE) {
+ return 0;
+ }
+ me = chess_player_color(g, who);
+ if (me == CHESS_COLOR_NONE) {
+ return 0;
+ }
+ if (g->draw_offered_by != CHESS_COLOR_NONE &&
+ g->draw_offered_by != me) {
+ g->result = CHESS_DRAW_AGREEMENT;
+ g->draw_offered_by = CHESS_COLOR_NONE;
+ return 1;
+ }
+ g->draw_offered_by = me;
+ return 0;
}
/* ── Board rendering ───────────────────────────────────────── */
@@ -478,75 +788,120 @@ 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;
+ unsigned r;
+ unsigned f;
- if (label && label[0]) hybbx_session_write_line(s, label);
- hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+");
+ if (label != NULL && label[0] != '\0') {
+ hybbx_session_write_line(s, label);
+ }
+ hybbx_session_write_line(s, " a b c d e f g h");
for (r = 0; r < 8; r++) {
unsigned rk = 7 - r;
- int n = snprintf(line, sizeof(line), "%u |", rk + 1);
+ 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);
+ char ch = (p == CHESS_EMPTY) ? '.' : p;
+
+ n += snprintf(line + n, sizeof(line) - (size_t)n, "%c ", ch);
}
+ n += snprintf(line + n, sizeof(line) - (size_t)n, "%u", rk + 1);
hybbx_session_write_line(s, line);
- hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+");
}
- hybbx_session_write_line(s, " a b c d e f g h");
+ hybbx_session_write_line(s, " a b c d e f g h");
+ hybbx_session_write_line(s, " (White=UPPER Black=lower)");
}
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;
+ char label[192];
+ const char *turn_name;
+
+ if (g == NULL || s == NULL) {
+ return;
+ }
+ turn_name = (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;
+ 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-1/2 Stalemate";
+ break;
+ case CHESS_DRAW_AGREEMENT:
+ rs = "1/2-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);
+ g->game_id, g->white_name,
+ g->black_session ? 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);
+ snprintf(label, sizeof(label),
+ "[%s] %s(W) vs %s(B) %s to move%s%s Move %u",
+ g->game_id, g->white_name,
+ g->black_session ? g->black_name : "...", turn_name,
+ g->last_move_san[0] ? " last=" : "",
+ g->last_move_san[0] ? g->last_move_san : "",
+ g->move_number);
}
render_board(&g->board, s, label);
+ if (g->draw_offered_by != CHESS_COLOR_NONE &&
+ g->result == CHESS_RESULT_NONE) {
+ snprintf(label, sizeof(label), "Draw offered by %s — /chess draw to accept",
+ color_name(g->draw_offered_by));
+ hybbx_session_write_line(s, label);
+ }
}
-/* ── Tick — 15s board broadcast ────────────────────────────── */
+void chess_broadcast_board(chess_game_t *g)
+{
+ unsigned j;
+
+ if (g == NULL) {
+ return;
+ }
+ if (g->white_session != NULL) {
+ chess_send_board(g, g->white_session);
+ }
+ if (g->black_session != NULL) {
+ chess_send_board(g, g->black_session);
+ }
+ for (j = 0; j < g->spectator_count; j++) {
+ chess_send_board(g, g->spectators[j]);
+ }
+}
+
+/* ── Tick — board refresh for spectators ───────────────────── */
void chess_tick(struct hybbx_service *service)
{
- unsigned i, j;
+ unsigned i;
+ unsigned 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;
+
+ if (!g->active || g->black_session == NULL ||
+ g->result != CHESS_RESULT_NONE || g->spectator_count == 0) {
+ continue;
+ }
g->tick_counter++;
- if (g->tick_counter < CHESS_REFRESH_SEC) continue;
+ 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++)
+ 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
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;
}
diff --git a/plugins/entertain/entertain.c b/plugins/entertain/entertain.c
index cf3e123..f8dfa6a 100644
--- a/plugins/entertain/entertain.c
+++ b/plugins/entertain/entertain.c
@@ -1,7 +1,7 @@
/*
* Entertain plugin — entertain.c
- * Plugin registration: init, shutdown, tick, on_command.
- * Chess is the first entertainment feature; more can be added.
+ * Feature plugin (not a transport): init / tick / on_command.
+ * Chess is the first Entertain feature. Main / standalone Main only.
*/
#include "hybbx/plugin.h"
#include "hybbx/service.h"
@@ -13,16 +13,26 @@
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 int verb_is_chess(const char *verb)
+{
+ if (verb == NULL) {
+ return 0;
+ }
+ return strcmp(verb, "chess") == 0 ||
+ strcmp(verb, "play") == 0 ||
+ strcmp(verb, "mv") == 0;
+}
+
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);
+ hybbx_log_info("[entertain] plugin loaded — chess_max_games=%u",
+ (unsigned)CHESS_DEFAULT_MAX);
return HYBBX_OK;
}
@@ -46,7 +56,7 @@ static hybbx_result_t entertain_on_command(struct hybbx_service *service,
return HYBBX_ERR_NOT_FOUND;
}
- if (strcmp(cmd->verb, "chess") == 0 || strcmp(cmd->verb, "play") == 0) {
+ if (verb_is_chess(cmd->verb)) {
return entertain_cmd_chess(service, session, cmd);
}
@@ -54,14 +64,14 @@ static hybbx_result_t entertain_on_command(struct hybbx_service *service,
}
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,
+ .name = "entertain",
+ .kind = 0, /* feature plugin — not a wire transport */
+ .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
index 9d7026c..55731b3 100644
--- a/plugins/entertain/entertain_chess.h
+++ b/plugins/entertain/entertain_chess.h
@@ -1,22 +1,24 @@
/*
- * Entertain plugin — chess.h (internal)
- * Chess game engine for HyBBX Entertain area.
+ * Entertain plugin — chess (internal)
+ * Chess for HyBBX Entertain area (Main only).
*/
#ifndef ENTERTAIN_CHESS_H
#define ENTERTAIN_CHESS_H
#include "hybbx/types.h"
+#include <stddef.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_REFRESH_SEC 30u
+#define CHESS_DEFAULT_MAX 8u
#define CHESS_NAME_MAX 32
-#define CHESS_SAN_MAX 8
+#define CHESS_SAN_MAX 16
#define CHESS_EMPTY '.'
typedef enum {
@@ -52,6 +54,7 @@ typedef struct chess_game {
char last_move_san[CHESS_SAN_MAX];
unsigned tick_counter;
char game_id[8];
+ chess_color_t draw_offered_by; /* NONE = no pending offer */
} chess_game_t;
/* Lifecycle */
@@ -64,25 +67,26 @@ 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);
+chess_game_t *chess_find_player_game(const struct hybbx_session *s);
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);
+void chess_release_game(chess_game_t *g);
+unsigned chess_clear_finished(void);
/* 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_broadcast_board(chess_game_t *g);
void chess_resign(chess_game_t *g, struct hybbx_session *who);
-void chess_offer_draw(chess_game_t *g, struct hybbx_session *who);
+/** Returns 1 if draw agreed, 0 if offer pending / rejected. */
+int chess_offer_draw(chess_game_t *g, struct hybbx_session *who);
-/* Tick — call from plugin tick every 1s */
+/* 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 c64352e..566f281 100644
--- a/share/commands.yaml
+++ b/share/commands.yaml
@@ -305,14 +305,14 @@ commands:
monitor:
group: monitor
min: Sysop
- line1: "/monitor on|off|list|cmds|meet user"
- line2: "Help: Live log monitor and sysop admin menu. More: /mon"
+ line1: "/monitor on|off|list|cmds|meet <user> [force]"
+ line2: "Help: Live log follow + online list; /monitor cmds for admin verbs. Alias: /mon"
chess:
group: entertain
- min: Guest
- line1: "/chess new|join|watch|move|resign|draw|list|board"
- line2: "Help: Play chess. Two players + spectators. More: /play /mv"
+ min: User
+ line1: "/chess new|join|watch|move|board|resign|draw|list|leave|clear"
+ line2: "Help: Entertain chess (Main). Moves: /chess move e2 e4 or /mv e2e4. Alias: /play"
diff --git a/share/hybbx-main.ini.example b/share/hybbx-main.ini.example
index 8573620..473f79c 100644
--- a/share/hybbx-main.ini.example
+++ b/share/hybbx-main.ini.example
@@ -56,6 +56,8 @@ ssh = yes
websocket = yes
circuit = yes
mains_proxy = no
+; Entertain (chess) — Main only; set yes to load plugin
+entertain = yes
[transport.telnet]
bind = 0.0.0.0
diff --git a/share/hybbx-standalone.ini.example b/share/hybbx-standalone.ini.example
index 86cff32..3353892 100644
--- a/share/hybbx-standalone.ini.example
+++ b/share/hybbx-standalone.ini.example
@@ -61,6 +61,8 @@ baycom = yes
tmodem = no
ssh = yes
circuit = yes
+; Entertain (chess) — Main only
+entertain = yes
[max25]
check = yes
diff --git a/share/hybbxd-main.ini.example b/share/hybbxd-main.ini.example
index efcadf6..1988b41 100644
--- a/share/hybbxd-main.ini.example
+++ b/share/hybbxd-main.ini.example
@@ -58,6 +58,8 @@ ardop = no
crdop = no
circuit = yes
mains_proxy = no
+; Entertain (chess) — Main / standalone Main only; AmigaOS build omits plugin
+entertain = yes
[transport.websocket]
bind = 127.0.0.1
diff --git a/src/core/command.c b/src/core/command.c
index 3614821..44485c7 100644
--- a/src/core/command.c
+++ b/src/core/command.c
@@ -2160,11 +2160,19 @@ static void monitor_list_visitor(hybbx_session_t *session, void *userdata)
static void monitor_show_cmds(hybbx_session_t *session)
{
- hybbx_session_write_line(session, "Monitor admin commands:");
+ hybbx_session_write_line(session, "Monitor:");
hybbx_session_write_line(session,
- " /monitor meet <user> [force] — conference invite (20s) or force");
+ " /monitor on|off — start/stop live log follow");
hybbx_session_write_line(session,
- "Run any of these as normal /verb (stay invisible while Sysop hidden):");
+ " /monitor list — status + visible online users");
+ hybbx_session_write_line(session,
+ " /monitor meet <user> [force] — conference invite (or force)");
+ hybbx_session_write_line(session,
+ " /monitor cmds — this menu + Sysop verbs");
+ hybbx_session_write_line(session,
+ "While monitor is on (or invisible-sysop=yes), Sysop stays hidden from /who.");
+ hybbx_session_write_line(session,
+ "Sysop verbs (use as normal /verb):");
hybbx_commands_registry_show_sysop_cmds(session);
}
@@ -2199,7 +2207,8 @@ static hybbx_result_t cmd_monitor(hybbx_service_t *service,
return hybbx_monitor_set_active(session, 0);
}
- if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands")) {
+ if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands") ||
+ str_ieq(sub, "help") || str_ieq(sub, "?")) {
monitor_show_cmds(session);
return HYBBX_OK;
}
@@ -2227,7 +2236,7 @@ static hybbx_result_t cmd_monitor(hybbx_service_t *service,
if (!str_ieq(sub, "list") && cmd->argc >= 1) {
hybbx_session_write_line(session,
- "Usage: /monitor on|off|list|cmds|meet <user> [force]");
+ "Usage: /monitor on|off|list|cmds|meet <user> [force] — /help monitor");
return HYBBX_ERR_INVALID;
}
diff --git a/src/core/instance.c b/src/core/instance.c
index 4640678..0b9f717 100644
--- a/src/core/instance.c
+++ b/src/core/instance.c
@@ -108,6 +108,11 @@ int hybbx_instance_plugin_allowed(const char *plugin_name)
return role != HYBBX_INSTANCE_MAIN;
}
+ /* Entertain (chess, …) is Main / standalone Main only. */
+ if (str_ieq(plugin_name, "entertain")) {
+ return role == HYBBX_INSTANCE_MAIN;
+ }
+
switch (role) {
case HYBBX_INSTANCE_MAIN:
if (str_ieq(plugin_name, "baycom") ||
@@ -155,7 +160,7 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
hybbx_instance_binary_name());
hybbx_log_info("[instance] binary=%s role=%s standalone=yes user_bbx=%s "
"telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s "
- "websocket=%s circuit=%s mains_proxy=%s",
+ "websocket=%s circuit=%s mains_proxy=%s entertain=%s",
hybbx_instance_binary_name(),
hybbx_instance_role_name(),
hybbx_instance_offers_user_bbx() ? "yes" : "no",
@@ -167,7 +172,8 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
hybbx_bool_to_string(networks->crdop),
hybbx_bool_to_string(networks->websocket),
hybbx_bool_to_string(networks->circuit),
- hybbx_bool_to_string(networks->mains_proxy));
+ hybbx_bool_to_string(networks->mains_proxy),
+ hybbx_bool_to_string(networks->entertain));
return HYBBX_OK;
}
@@ -212,6 +218,10 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
break;
case HYBBX_INSTANCE_SECONDARY:
+ if (networks->entertain) {
+ hybbx_log_warn("[instance] hybbxsd: entertain Main-only — forcing off");
+ networks->entertain = 0;
+ }
if (networks->telnet) {
hybbx_log_warn("[instance] hybbxsd: no user telnet — forcing off");
networks->telnet = 0;
@@ -233,6 +243,10 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
break;
case HYBBX_INSTANCE_PROXY:
+ if (networks->entertain) {
+ hybbx_log_warn("[instance] hybbxpd: entertain Main-only — forcing off");
+ networks->entertain = 0;
+ }
if (networks->websocket) {
hybbx_log_warn("[instance] hybbxpd: WebSocket forbidden — forcing off");
networks->websocket = 0;
@@ -254,7 +268,7 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
hybbx_log_info("[instance] binary=%s role=%s user_bbx=%s "
"telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s "
- "websocket=%s circuit=%s mains_proxy=%s",
+ "websocket=%s circuit=%s mains_proxy=%s entertain=%s",
hybbx_instance_binary_name(),
hybbx_instance_role_name(),
hybbx_instance_offers_user_bbx() ? "yes" : "no",
@@ -266,7 +280,8 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks
hybbx_bool_to_string(networks->crdop),
hybbx_bool_to_string(networks->websocket),
hybbx_bool_to_string(networks->circuit),
- hybbx_bool_to_string(networks->mains_proxy));
+ hybbx_bool_to_string(networks->mains_proxy),
+ hybbx_bool_to_string(networks->entertain));
return HYBBX_OK;
}
diff --git a/src/core/networks.c b/src/core/networks.c
index c52554d..f61479e 100644
--- a/src/core/networks.c
+++ b/src/core/networks.c
@@ -54,6 +54,7 @@ void hybbx_networks_config_defaults(hybbx_networks_config_t *networks)
networks->websocket = 0;
networks->circuit = 1;
networks->mains_proxy = 0;
+ networks->entertain = 0;
}
void hybbx_networks_config_apply(hybbx_networks_config_t *networks,
@@ -106,8 +107,13 @@ void hybbx_networks_config_apply(hybbx_networks_config_t *networks,
"mains_proxy", 0);
}
+ if (networks_has_key(config, "entertain")) {
+ networks->entertain = hybbx_config_get_bool(config, "networks",
+ "entertain", 0);
+ }
+
hybbx_log_info("[networks] telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s "
- "crdop=%s websocket=%s circuit=%s mains_proxy=%s",
+ "crdop=%s websocket=%s circuit=%s mains_proxy=%s entertain=%s",
hybbx_bool_to_string(networks->telnet),
hybbx_bool_to_string(networks->ssh),
hybbx_bool_to_string(networks->ax25),
@@ -116,7 +122,8 @@ void hybbx_networks_config_apply(hybbx_networks_config_t *networks,
hybbx_bool_to_string(networks->crdop),
hybbx_bool_to_string(networks->websocket),
hybbx_bool_to_string(networks->circuit),
- hybbx_bool_to_string(networks->mains_proxy));
+ hybbx_bool_to_string(networks->mains_proxy),
+ hybbx_bool_to_string(networks->entertain));
}
int hybbx_networks_is_static_transport(const char *plugin_name)
@@ -187,5 +194,9 @@ int hybbx_networks_transport_wanted(const char *plugin_name,
return networks->mains_proxy;
}
+ if (str_ieq(plugin_name, "entertain")) {
+ return networks->entertain;
+ }
+
return 0;
}
diff --git a/src/core/service.c b/src/core/service.c
index 20934a7..dda6311 100644
--- a/src/core/service.c
+++ b/src/core/service.c
@@ -918,11 +918,8 @@ hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service,
return HYBBX_ERR_BUSY;
}
- if (active->plugin->start == NULL) {
- return HYBBX_ERR_UNSUPPORTED;
- }
-
- {
+ /* Feature plugins (e.g. entertain) may have init/tick/on_command only. */
+ if (active->plugin->start != NULL) {
hybbx_result_t rc = active->plugin->start(config);
if (rc != HYBBX_OK) {
return rc;
@@ -1040,8 +1037,9 @@ hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service,
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;
+ /* NOT_FOUND = not this plugin; any other result = handled. */
+ if (rc != HYBBX_ERR_NOT_FOUND) {
+ return rc;
}
}
}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com