feat(gems): add multiplayer gem game with custom content packs
treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 2m25s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-rhapsody-in-green (push) Successful in 49s

- implement FastAPI and HTMX lobby and game interfaces
- support up to four human and AI-controlled players
- add configurable rules, victory conditions, and expansion modules
- support validated JSON cards, patrons, objectives, and outposts
This commit was merged in pull request #43.
This commit is contained in:
2026-07-22 20:47:03 -04:00
parent 247e951a27
commit add7a6a848
41 changed files with 4161 additions and 1 deletions
+52
View File
@@ -0,0 +1,52 @@
{
inputs,
pkgs,
...
}:
let
vars = import ../vars.nix;
stateDir = "${vars.services}/gems";
in
{
users.groups.gems = { };
users.users.gems = {
isSystemUser = true;
group = "gems";
home = stateDir;
};
systemd.tmpfiles.rules = [
"d ${stateDir} 0750 gems gems - -"
];
systemd.services.gems = {
description = "Gems multiplayer game";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
PYTHONPATH = "${inputs.self}";
GEMS_DATABASE_PATH = "${stateDir}/gems.sqlite3";
GEMS_KEY_PATH = "${stateDir}/instance.key";
GEMS_PUBLIC_ORIGIN = "https://gems.tmmworkshop.com";
GEMS_SECURE_COOKIES = "true";
};
serviceConfig = {
Type = "simple";
User = "gems";
Group = "gems";
ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 127.0.0.1 --port 8002";
Restart = "on-failure";
RestartSec = "5s";
StandardOutput = "journal";
StandardError = "journal";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadOnlyPaths = [ "${inputs.self}" ];
ReadWritePaths = [ stateDir ];
};
};
}
+1
View File
@@ -3,6 +3,7 @@ let
"audiobookshelf"
"cache"
"gitea"
"gems"
"jellyfin"
"share"
];
+21 -1
View File
@@ -23,7 +23,7 @@ defaults
#Application Setup
frontend ContentSwitching
bind *:80 v4v6
bind *:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem
bind *:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem
mode http
# ACME challenge routing (must be first)
@@ -35,6 +35,7 @@ frontend ContentSwitching
acl host_jellyfin hdr(host) -i jellyfin.tmmworkshop.com
acl host_share hdr(host) -i share.tmmworkshop.com
acl host_gitea hdr(host) -i gitea.tmmworkshop.com
acl host_gems hdr(host) -i gems.tmmworkshop.com
acl host_norn_sight hdr(host) -i www.norn-sight.com
# --- Request logging ---
@@ -84,6 +85,15 @@ frontend ContentSwitching
http-request track-sc1 src table st_compare if host_gitea is_gitea_compare !rate_limit_allowlist
http-request deny deny_status 429 if host_gitea is_gitea_compare !rate_limit_allowlist { sc_http_req_rate(1,st_compare) gt 1 }
# Anonymous Gems rooms use high-entropy invitation codes, with an additional
# per-IP limit on room creation and join attempts.
acl gems_entry path -i /rooms
acl gems_join path_beg -i /join
acl request_post method POST
http-request track-sc2 src table st_gems_join if host_gems request_post gems_entry
http-request track-sc2 src table st_gems_join if host_gems request_post gems_join
http-request deny deny_status 429 if host_gems { sc_http_req_rate(2,st_gems_join) gt 20 }
# Hosts allowed to serve plain HTTP (add entries to skip the HTTPS redirect)
acl allow_http hdr(host) -i __none__
# acl allow_http hdr(host) -i example.tmmworkshop.com
@@ -97,6 +107,7 @@ frontend ContentSwitching
use_backend jellyfin if host_jellyfin
use_backend share_nodes if host_share
use_backend gitea if host_gitea
use_backend gems if host_gems
use_backend norn_sight if host_norn_sight
# Stick-table only (no servers): tracks per-IP request rate to Gitea's compare
@@ -104,6 +115,9 @@ frontend ContentSwitching
backend st_compare
stick-table type ipv6 size 100k expire 600s store http_req_rate(300s)
backend st_gems_join
stick-table type ipv6 size 100k expire 120s store http_req_rate(60s)
backend acme_challenge
mode http
server acme 127.0.0.1:8402
@@ -129,6 +143,12 @@ backend gitea
mode http
server server 127.0.0.1:6443
backend gems
mode http
option forwardfor
timeout server 1h
server gems 127.0.0.1:8002
backend norn_sight
mode http
server server 127.0.0.1:8001