refactor: move Python packages under python_pkg/

- Move puzzle_solver/, poker_modifier_app/, articles/, tests/ into python_pkg/
- Move moviepy_showcase.py and _moviepy_*.py into python_pkg/moviepy_showcase/
- Update all imports to use python_pkg. prefix
- Update pyproject.toml per-file-ignores and pytest testpaths
- Add pre-commit hook to enforce Python files under python_pkg/
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-18 22:54:45 +01:00
parent 78c1d77144
commit 4cf523bf6d
35 changed files with 6620 additions and 0 deletions

View File

@ -0,0 +1,16 @@
BasedOnStyle: LLVM
Language: Cpp
ColumnLimit: 100
ReflowComments: true
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
AllowShortLoopsOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
BinPackArguments: true
BinPackParameters: true
BreakBeforeBraces: Attach
PointerAlignment: Left
IndentWidth: 2
TabWidth: 2
UseTab: Never
SortIncludes: false

2
python_pkg/articles/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
data/*
uploads/*

View File

@ -0,0 +1,61 @@
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra -pedantic
LDFLAGS ?=
.ONESHELL:
all: server_c
server_c: server_c.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
format:
@command -v clang-format >/dev/null 2>&1 || { echo "clang-format not found"; exit 1; }
@echo "Running clang-format (ColumnLimit=100)..."
@clang-format -i -style=file server_c.c
format-check:
@command -v clang-format >/dev/null 2>&1 || { echo "clang-format not found"; exit 1; }
@echo "Checking formatting with clang-format..."
@# Use clang-format dry-run so this works without git and doesn't depend on commit state
@if clang-format --dry-run -Werror -style=file server_c.c >/dev/null 2>&1; then \
echo "Format check passed"; \
else \
echo "Formatting changes needed (run 'make format')"; \
exit 1; \
fi
# fail if any function exceeds 20 non-empty lines between '{' and matching '}'
funcsize-check:
@echo "Checking function sizes (<= 20 lines)..."
@command -v clang-tidy >/dev/null 2>&1 || { echo "clang-tidy not found"; exit 1; }
@clang-tidy \
-checks=readability-function-size \
-warnings-as-errors=readability-function-size \
-config='{ "CheckOptions": [ { "key": "readability-function-size.LineThreshold", "value": "20" } ] }' \
server_c.c -- -std=gnu11 $(CFLAGS) $(LDFLAGS)
lint:
@echo "Running clang-tidy..."
@clang-tidy \
-checks=clang-analyzer-*,clang-diagnostic-*,readability-function-size,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling \
-config='{ "CheckOptions": [ { "key": "readability-function-size.LineThreshold", "value": "20" } ] }' \
server_c.c -- -std=gnu11 $(CFLAGS) $(LDFLAGS) || true
@echo "Running cppcheck..."
@cppcheck --enable=all --inconclusive --std=c11 --language=c --quiet --inline-suppr --check-level=exhaustive --suppress=missingIncludeSystem server_c.c || true
@echo "Checking line length (<= 100 chars) in server_c.c..."
@awk 'length($$0)>100{print "server_c.c:" NR ": line too long (" length($$0) " > 100)"; err=1} END{if(err){print "Line length check FAILED"; exit 1} else {print "Line length check passed"}}' server_c.c
build: minify
minify:
npx -y html-minifier-terser \
--collapse-whitespace --remove-comments --remove-attribute-quotes \
--remove-redundant-attributes --minify-css true --minify-js true \
-o index.min.html index.html
npx -y terser sw.js -o sw.min.js -c -m
clean:
rm -f server_c
.PHONY: all clean

View File

@ -0,0 +1,17 @@
Mini Articles (<=14KB)
- Single-file site: `index.html` with inline CSS & JS
- Features:
- List of articles with thumbnails (cards)
- Read view: thumbnail, title, body (supports inline images/videos)
- Create view: title, thumbnail picker/drag-drop, rich body via contenteditable
- Drag/drop or choose images/videos anywhere in the body
- Local persistence via localStorage (no server required)
How to open
- Open `site/index.html` in a browser.
Tests
- `pytest` includes a test to enforce the 14KB budget for `index.html`.

View File

@ -0,0 +1 @@
"""Articles site tests package."""

View File

@ -0,0 +1,478 @@
server_c.c:2:0: information: Include file: <arpa/inet.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <arpa/inet.h>
^
server_c.c:3:0: information: Include file: <errno.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <errno.h>
^
server_c.c:4:0: information: Include file: <ctype.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <ctype.h>
^
server_c.c:5:0: information: Include file: <fcntl.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <fcntl.h>
^
server_c.c:6:0: information: Include file: <netinet/in.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <netinet/in.h>
^
server_c.c:7:0: information: Include file: <signal.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <signal.h>
^
server_c.c:8:0: information: Include file: <stdbool.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <stdbool.h>
^
server_c.c:9:0: information: Include file: <stdio.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <stdio.h>
^
server_c.c:10:0: information: Include file: <stdlib.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <stdlib.h>
^
server_c.c:11:0: information: Include file: <string.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <string.h>
^
server_c.c:12:0: information: Include file: <sys/socket.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <sys/socket.h>
^
server_c.c:13:0: information: Include file: <sys/stat.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <sys/stat.h>
^
server_c.c:14:0: information: Include file: <sys/types.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <sys/types.h>
^
server_c.c:15:0: information: Include file: <time.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <time.h>
^
server_c.c:16:0: information: Include file: <unistd.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <unistd.h>
^
server_c.c:0:0: information: Limiting analysis of branches. Use --check-level=exhaustive to analyze all branches. [normalCheckLevelMaxBranches]
^
server_c.c:411:77: warning: Either the condition 'elen<8' is redundant or the array 'ext[4]' is accessed at index 7, which is out of bounds. [arrayIndexOutOfBoundsCond]
size_t elen=0; while(ext[elen] && elen<8 && isalnum((unsigned char)ext[elen])) elen++; char* ext_safe=strndup_local(ext,elen?elen:3); if(!ext_safe){ send_response(c,500,"Internal Server Error","application/json","",0,true); return; }
^
server_c.c:411:45: note: Assuming that condition 'elen<8' is not redundant
size_t elen=0; while(ext[elen] && elen<8 && isalnum((unsigned char)ext[elen])) elen++; char* ext_safe=strndup_local(ext,elen?elen:3); if(!ext_safe){ send_response(c,500,"Internal Server Error","application/json","",0,true); return; }
^
server_c.c:411:77: note: Array index out of bounds
size_t elen=0; while(ext[elen] && elen<8 && isalnum((unsigned char)ext[elen])) elen++; char* ext_safe=strndup_local(ext,elen?elen:3); if(!ext_safe){ send_response(c,500,"Internal Server Error","application/json","",0,true); return; }
^
server_c.c:411:31: style: Array index 'elen' is used before limits check. [arrayIndexThenCheck]
size_t elen=0; while(ext[elen] && elen<8 && isalnum((unsigned char)ext[elen])) elen++; char* ext_safe=strndup_local(ext,elen?elen:3); if(!ext_safe){ send_response(c,500,"Internal Server Error","application/json","",0,true); return; }
^
server_c.c:314:129: style: Condition 'tcontent[0]=='['' is always true [knownConditionTrueFalse]
char* tcontent=ltrim_dup(content); if(tcontent && tcontent[0]=='['){ size_t clen=strlen(tcontent); if(clen>=2 && tcontent[0]=='[' && tcontent[clen-1]==']'){
^
server_c.c:314:66: note: Assuming that condition 'tcontent[0]=='['' is not redundant
char* tcontent=ltrim_dup(content); if(tcontent && tcontent[0]=='['){ size_t clen=strlen(tcontent); if(clen>=2 && tcontent[0]=='[' && tcontent[clen-1]==']'){
^
server_c.c:314:129: note: Condition 'tcontent[0]=='['' is always true
char* tcontent=ltrim_dup(content); if(tcontent && tcontent[0]=='['){ size_t clen=strlen(tcontent); if(clen>=2 && tcontent[0]=='[' && tcontent[clen-1]==']'){
^
server_c.c:463:6: warning: inconclusive: Width 1023 given in format string (no. 2) is smaller than destination buffer 'path[4096]'. [invalidScanfFormatWidth_smaller]
if(sscanf(buf, "%15s %1023s", method, path) < 2){ close(c); return; }
^
server_c.c:289:37: error: Common realloc mistake: 'objs' nulled but not freed upon failure [memleakOnRealloc]
if(count==cap){ cap*=2; objs=realloc(objs, cap*sizeof(char*)); }
^
server_c.c:369:39: error: Common realloc mistake: 'objs' nulled but not freed upon failure [memleakOnRealloc]
if(count==cap){ cap*=2; objs=realloc(objs, cap*sizeof(char*)); }
^
server_c.c:42:136: warning: If memory allocation fails, then there is a possible null pointer dereference: data [nullPointerOutOfMemory]
static int write_file_all(const char* path, const char* data, size_t len){ FILE* f=fopen(path,"wb"); if(!f) return -1; size_t n=fwrite(data,1,len,f); fclose(f); return n==len?0:-1; }
^
server_c.c:312:59: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:169: note: Calling function 'write_file_all', 2nd argument 'arr' value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:42:105: note: Assuming condition is false
static int write_file_all(const char* path, const char* data, size_t len){ FILE* f=fopen(path,"wb"); if(!f) return -1; size_t n=fwrite(data,1,len,f); fclose(f); return n==len?0:-1; }
^
server_c.c:42:136: note: Null pointer dereference
static int write_file_all(const char* path, const char* data, size_t len){ FILE* f=fopen(path,"wb"); if(!f) return -1; size_t n=fwrite(data,1,len,f); fclose(f); return n==len?0:-1; }
^
server_c.c:64:21: warning: If memory allocation fails, then there is a possible null pointer dereference: p [nullPointerOutOfMemory]
while((p = strstr(p, qkey))){
^
server_c.c:358:187: note: Assuming allocation function fails
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:360:40: note: Calling function 'json_get_string', 1st argument 'obj' value is 0
char* id=json_get_string(obj, "id"); char* title=json_get_string(obj, "title"); char* author=json_get_string(obj, "author"); char* body_s=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); long long createdAt=json_get_number(obj, "\"createdAt\"");
^
server_c.c:59:8: note: Assuming condition is false
if(!tmp) return strdup("");
^
server_c.c:63:19: note: Assignment 'p=json', assigned value is 0
const char* p = json;
^
server_c.c:64:21: note: Null pointer dereference
while((p = strstr(p, qkey))){
^
server_c.c:76:93: warning: If memory allocation fails, then there is a possible null pointer dereference: json [nullPointerOutOfMemory]
static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); }
^
server_c.c:358:187: note: Assuming allocation function fails
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:360:262: note: Calling function 'json_get_number', 1st argument 'obj' value is 0
char* id=json_get_string(obj, "id"); char* title=json_get_string(obj, "title"); char* author=json_get_string(obj, "author"); char* body_s=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); long long createdAt=json_get_number(obj, "\"createdAt\"");
^
server_c.c:76:93: note: Null pointer dereference
static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); }
^
server_c.c:268:261: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:241: note: Assuming allocation function fails
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:241: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:261: note: Null pointer dereference
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:285: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:241: note: Assuming allocation function fails
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:241: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:268:285: note: Null pointer dereference
size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0'; char* got=json_get_string(obj, "\"id\""); int match=got && strcmp(got,id)==0; free(got); if(match){ free(t); return obj; } free(obj); }
^
server_c.c:283:198: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:178: note: Assuming allocation function fails
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:178: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:198: note: Null pointer dereference
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:222: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:178: note: Assuming allocation function fails
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:178: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:283:222: note: Null pointer dereference
for(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:299:46: warning: If memory allocation fails, then there is a possible null pointer dereference: out [nullPointerOutOfMemory]
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t L=strlen(objs[k]); memcpy(out+w, objs[k], L); w+=L; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:299:19: note: Assuming allocation function fails
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t L=strlen(objs[k]); memcpy(out+w, objs[k], L); w+=L; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:299:19: note: Assignment 'out=malloc(total_len+1)', assigned value is 0
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t L=strlen(objs[k]); memcpy(out+w, objs[k], L); w+=L; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:299:46: note: Null pointer dereference
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t L=strlen(objs[k]); memcpy(out+w, objs[k], L); w+=L; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:312:80: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:80: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:121: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:121: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:135: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:135: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:319:49: warning: If memory allocation fails, then there is a possible null pointer dereference: out [nullPointerOutOfMemory]
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assuming allocation function fails
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assignment 'out=malloc(newlen+1)', assigned value is 0
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:49: note: Null pointer dereference
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:172: warning: If memory allocation fails, then there is a possible null pointer dereference: out [nullPointerOutOfMemory]
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assuming allocation function fails
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assignment 'out=malloc(newlen+1)', assigned value is 0
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:172: note: Null pointer dereference
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:186: warning: If memory allocation fails, then there is a possible null pointer dereference: out [nullPointerOutOfMemory]
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assuming allocation function fails
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assignment 'out=malloc(newlen+1)', assigned value is 0
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:186: note: Null pointer dereference
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:321:84: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:84: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:125: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:125: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:139: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:139: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:82: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:82: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:123: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:123: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:137: warning: If memory allocation fails, then there is a possible null pointer dereference: arr [nullPointerOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:137: note: Null pointer dereference
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:358:207: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assuming allocation function fails
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:207: note: Null pointer dereference
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:231: warning: If memory allocation fails, then there is a possible null pointer dereference: obj [nullPointerOutOfMemory]
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assuming allocation function fails
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:231: note: Null pointer dereference
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:363:331: warning: If memory allocation fails, then there is a possible null pointer dereference: thumb [nullPointerOutOfMemory]
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); }
^
server_c.c:363:314: note: Assuming allocation function fails
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); }
^
server_c.c:363:314: note: Assignment 'thumb=malloc(urlL)', assigned value is 0
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); }
^
server_c.c:363:331: note: Null pointer dereference
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); }
^
server_c.c:376:52: warning: If memory allocation fails, then there is a possible null pointer dereference: out [nullPointerOutOfMemory]
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t Lx=strlen(objs[k]); memcpy(out+w, objs[k], Lx); w+=Lx; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:376:25: note: Assuming allocation function fails
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t Lx=strlen(objs[k]); memcpy(out+w, objs[k], Lx); w+=Lx; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:376:25: note: Assignment 'out=malloc(total_len+1)', assigned value is 0
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t Lx=strlen(objs[k]); memcpy(out+w, objs[k], Lx); w+=Lx; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:376:52: note: Null pointer dereference
char* out=malloc(total_len+1); size_t w=0; out[w++]='['; for(size_t k=0;k<count;k++){ size_t Lx=strlen(objs[k]); memcpy(out+w, objs[k], Lx); w+=Lx; if(k+1<count) out[w++]=','; } out[w++]=']'; out[w]='\0';
^
server_c.c:384:325: warning: If memory allocation fails, then there is a possible null pointer dereference: thumb [nullPointerOutOfMemory]
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); } }
^
server_c.c:384:308: note: Assuming allocation function fails
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); } }
^
server_c.c:384:308: note: Assignment 'thumb=malloc(urlL)', assigned value is 0
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); } }
^
server_c.c:384:325: note: Null pointer dereference
if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); } }
^
server_c.c:312:104: error: If memory allocation fails: pointer addition with NULL pointer. [nullPointerArithmeticOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:59: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:312:104: note: Null pointer addition
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:319:73: error: If memory allocation fails: pointer addition with NULL pointer. [nullPointerArithmeticOutOfMemory]
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assuming allocation function fails
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:25: note: Assignment 'out=malloc(newlen+1)', assigned value is 0
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:319:73: note: Null pointer addition
char* out=malloc(newlen+1); size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out);
^
server_c.c:321:108: error: If memory allocation fails: pointer addition with NULL pointer. [nullPointerArithmeticOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:63: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:321:108: note: Null pointer addition
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:106: error: If memory allocation fails: pointer addition with NULL pointer. [nullPointerArithmeticOutOfMemory]
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assuming allocation function fails
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:61: note: Assignment 'arr=malloc(tot+1)', assigned value is 0
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:325:106: note: Null pointer addition
size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr);
^
server_c.c:136:159: style: Variable 'h' can be declared as const array [constVariable]
size_t L=strlen(payload); bytes=malloc(L+1); if(!bytes){ free(mime); return -1; } size_t w=0; for(size_t i=0;i<L;i++){ if(payload[i]=='%' && i+2<L){ char h[3]={payload[i+1],payload[i+2],'\0'}; bytes[w++]=(unsigned char)strtol(h,NULL,16); i+=2; } else if(payload[i]=='+'){ bytes[w++]=' '; } else { bytes[w++]=(unsigned char)payload[i]; } } blen=w; }
^
server_c.c:465:34: style: Variable 'cl' can be declared as pointer to const [constVariablePointer]
size_t content_length=0; char* cl = strcasestr(buf, "Content-Length:"); if(cl){ content_length = strtoul(cl+15, NULL, 10); }
^
server_c.c:468:9: style: Variable 'hdr_end' can be declared as pointer to const [constVariablePointer]
char* hdr_end = strstr(buf, "\r\n\r\n"); size_t header_bytes = hdr_end? (size_t)(hdr_end - buf) + 4 : (size_t)total; size_t have_body = total > (ssize_t)header_bytes ? (size_t)total - header_bytes : 0;
^
server_c.c:355:47: style: Variable 'L2' is assigned a value that is never used. [unreadVariable]
if(t[0] != '['){ free(file); size_t L2=2; send_response(c,200,"OK","application/json","[]",2,true); free(t); return; }
^
server_c.c:355:45: style: Variable 'L2' is assigned a value that is never used. [unreadVariable]
if(t[0] != '['){ free(file); size_t L2=2; send_response(c,200,"OK","application/json","[]",2,true); free(t); return; }
^
server_c.c:76:93: warning: If memory allocation fails, then there is a possible null pointer dereference: json [ctunullpointerOutOfMemory]
static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); }
^
server_c.c:358:187: note: Assuming allocation function fails
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:358:187: note: Assignment 'obj=malloc(obj_len+1)', assigned value is 0
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
^
server_c.c:360:261: note: Calling function json_get_number, 1st argument is null
char* id=json_get_string(obj, "id"); char* title=json_get_string(obj, "title"); char* author=json_get_string(obj, "author"); char* body_s=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); long long createdAt=json_get_number(obj, "\"createdAt\"");
^
server_c.c:76:93: note: Dereferencing argument json that is null
static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); }
^
server_c.c:44:12: style: The function 'append_file_line' is never used. [unusedFunction]
static int append_file_line(const char* path, const char* line){ FILE* f=fopen(path,"ab"); if(!f) return -1; size_t n=fwrite(line,1,strlen(line),f); n+=fwrite("\n",1,1,f); fclose(f); return (int)n>=0?0:-1; }
^
nofile:0:0: information: Active checkers: 117/966 (use --checkers-report=<filename> to see details) [checkersReport]

View File

@ -0,0 +1,24 @@
[
{
"id": "29176c917f1a66c3",
"title": "full featuresd article",
"author": "author",
"body": "<img loading=\"lazy\" decoding=\"async\" src=\"/uploads/29176c9e5c008daf.jpg\" alt=\"image\"><div><br></div><div>This is an important image of course :)</div><div><br></div><div>and nother:<br><img loading=\"lazy\" decoding=\"async\" src=\"/uploads/29176c9f62367245.jpg\" alt=\"image\"><br></div>",
"thumb": "/uploads/29176c9e7818ee30.jpg",
"createdAt": 1757331025041
},
{
"id": "199259f41b8933b8",
"title": "Whats heveier",
"body": "A kilogrem of stel or kilogrem of fethers",
"thumb": "/uploads/27d6402e78a09d65.jpg",
"createdAt": 1757272818104
},
{
"id": "19925965dead21e7",
"title": "UwU its my first article",
"body": ":)))<div><img src=\"/uploads/27d640335a16f85f.jpg\"><br></div>",
"thumb": "/uploads/27d640310e163446.jpg",
"createdAt": 1757272235498
}
]

View File

@ -0,0 +1,233 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mini Articles</title>
<style>
*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:16px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;color:#222;background:#fff}
header{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.5rem .75rem;border-bottom:1px solid #ddd;position:sticky;top:0;background:#fff}
header h1{margin:0;font-size:1.1rem}
header nav{display:flex;gap:.5rem}
button, input[type=file]{font:inherit}
main{max-width:960px;margin:0 auto;padding:1rem}
.hidden{display:none!important}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem}
.card{border:1px solid #ddd;border-radius:.5rem;overflow:hidden;background:#fff;cursor:pointer;display:flex;flex-direction:column}
.card img{width:100%;height:120px;object-fit:cover;display:block}
.card h3{margin:.5rem;font-size:1rem}
#editor label{display:block;margin:.5rem 0 .25rem}
input[type=text]{width:100%;padding:.5rem;border:1px solid #ccc;border-radius:.4rem}
.drop{border:1.5px dashed #bbb;border-radius:.5rem;padding:.5rem;text-align:center;color:#666}
.drop img{max-width:100%;max-height:200px;display:block;margin:.25rem auto}
#content{min-height:200px;padding:.5rem;border:1px solid #ccc;border-radius:.5rem}
#content:empty:before{content:attr(data-ph);color:#999}
article img, article video{max-width:100%;height:auto;display:block;margin:.5rem 0}
article h1{margin:.25rem 0 .5rem;font-size:1.6rem}
.controls{display:flex;gap:.5rem;flex-wrap:wrap;margin:.5rem 0}
.small{font-size:.85rem;color:#666}
#loading{display:flex;align-items:center;gap:.5rem;margin:0 0 1rem}
.spinner{width:16px;height:16px;border:2px solid #ccc;border-top-color:#333;border-radius:50%;display:inline-block;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.reading header nav{display:none}
.reading #delBtn{display:none}
</style>
<header>
<h1>Mini Articles</h1>
<nav>
<button id="toList" title="All articles">List</button>
<button id="toNew" title="Create article">New</button>
</nav>
</header>
<main>
<section id="loading" class="hidden">
<div class="small"><span class="spinner"></span><span class="msg">Loading…</span></div>
</section>
<section id="listV" class="grid"></section>
<section id="readV" class="hidden">
<div class="controls">
<button id="backBtn">← Back</button>
<button id="delBtn">Delete</button>
<span class="small" id="dateInfo"></span>
</div>
<article id="readA">
<img id="readThumb"/>
<h1 id="readTitle"></h1>
<div id="readBody"></div>
</article>
</section>
<section id="editor" class="hidden">
<label>Title</label>
<input id="title" type="text" placeholder="Article title"/>
<label>Author</label>
<input id="author" type="text" placeholder="Author (optional)"/>
<label>Thumbnail</label>
<div id="thumbDrop" class="drop">Choose image…<br><img id="thumbPrev"/></div>
<input id="thumbFile" type="file"/>
<label>Body</label>
<div class="controls">
<button id="insImg">Insert image</button>
<button id="insVid">Insert video</button>
</div>
<div id="content" contenteditable="true" data-ph="Write here, anything html-able should work"></div>
<div class="controls">
<button id="saveBtn">Save</button>
<button id="cancelBtn">Cancel</button>
</div>
<input id="imgFile" type="file" hidden>
<input id="vidFile" type="file" hidden>
</section>
</main>
<script>
(function(){
const $=s=>document.querySelector(s);
const listV=$('#listV'), readV=$('#readV'), editor=$('#editor');
const loading=$('#loading');
const rT=$('#readTitle'), rB=$('#readBody'), rTh=$('#readThumb'), dt=$('#dateInfo');
const toList=$('#toList'), toNew=$('#toNew'), back=$('#backBtn'), del=$('#delBtn');
const title=$('#title'), author=$('#author'), content=$('#content');
const tDrop=$('#thumbDrop'), tPrev=$('#thumbPrev'), tFile=$('#thumbFile');
const insImg=$('#insImg'), insVid=$('#insVid'), imgFile=$('#imgFile'), vidFile=$('#vidFile');
const saveBtn=$('#saveBtn'), cancelBtn=$('#cancelBtn');
const KEY='articles14k';
const PH="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 120'%3E%3Crect width='100%25' height='100%25' fill='%23eee'/%3E%3C/svg%3E";
let selId=null; let API=null; let preList; // cache first list to avoid duplicate initial fetches
const AC={};
function setLoading(on,msg){ if(!loading) return; if(msg) loading.querySelector('.msg').textContent=msg; loading.classList.toggle('hidden',!on); }
async function uploadBlobAndGetURL(blob){
try{
const ext = blob.type.startsWith('image/')? (blob.type.split('/')[1]||'bin') : 'bin';
const r = await fetch(`/api/upload?ext=${encodeURIComponent(ext)}`, { method:'POST', headers:{'Content-Type': blob.type || 'application/octet-stream'}, body: blob });
if(!r.ok) throw new Error('upload failed');
const j = await r.json();
return j && j.url ? j.url : '';
}catch(e){ return ''; }
}
function imgEl(src){ const img=new Image(); img.loading='lazy'; img.decoding='async'; img.src=src; return img; }
function videoEl(src){ const v=document.createElement('video'); v.controls=true; v.src=src; v.style.maxWidth='100%'; return v; }
// API client with detection
async function detectAPI(){
// Try server once; cache the result to reuse for initial render
try{
const r=await fetch('/api/articles');
if(r.ok){
preList = await r.json();
return {
async list(){ return (await fetch('/api/articles')).json() },
async get(id){const r=await fetch('/api/articles/'+id); if(!r.ok) return null; return r.json()},
async create(a){const r=await fetch('/api/articles',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(a)}); return r.json()},
async update(id,patch){const r=await fetch('/api/articles/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)}); return r.ok? r.json():null},
async remove(id){await fetch('/api/articles/'+id,{method:'DELETE'})}
};
}
}catch(e){}
// Fallback to localStorage
const S={get(){try{return JSON.parse(localStorage.getItem(KEY)||'[]')}catch(e){return []}},set(a){localStorage.setItem(KEY,JSON.stringify(a))}};
const uid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6);
return {
async list(){return S.get()},
async get(id){return S.get().find(x=>x.id===id)||null},
async create(a){const arr=S.get(); const obj=Object.assign({id:uid(),createdAt:Date.now()},a); arr.unshift(obj); S.set(arr); return obj},
async update(id,patch){const arr=S.get(); const i=arr.findIndex(x=>x.id===id); if(i<0) return null; arr[i]=Object.assign(arr[i],patch,{updatedAt:Date.now()}); S.set(arr); return arr[i]},
async remove(id){const arr=S.get().filter(x=>x.id!==id); S.set(arr)}
};
}
function show(v){[listV,readV,editor].forEach(x=>x.classList.add('hidden')); v.classList.remove('hidden'); document.documentElement.classList.toggle('reading', v===readV)}
async function renderList(items){
let a;
if(items!==undefined){
a = items;
} else {
setLoading(true,'Loading articles…');
try{ a = await API.list(); }
finally{ setLoading(false); }
}
if(!a.length){listV.innerHTML='<div class="small">No articles yet. Click New to create one.</div>';return}
listV.innerHTML=a.map(x=>`<div class="card" data-id="${x.id}"><img loading="lazy" decoding="async" src="${x.thumb||PH}" alt="thumb"><h3>${esc(x.title)}</h3></div>`).join('');
}
function esc(s){return (s||'').replace(/[&<>]/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[m]))}
function pick(el,cb){el.onchange=e=>{const f=e.target.files&&e.target.files[0]; if(f) fileToDataURL(f).then(cb)}; el.click()}
function fileToDataURL(f){return new Promise((res,rej)=>{const r=new FileReader(); r.onload=()=>res(r.result); r.onerror=rej; r.readAsDataURL(f)})}
function dataURLToBlob(dataURL){ try{ const [h,b]=dataURL.split(','); const m=h.match(/data:([^;]+)(;base64)?/); if(!m) return null; const mime=m[1]; const isB64=!!m[2]; const raw=isB64? atob(b): decodeURIComponent(b); const arr=new Uint8Array(raw.length); for(let i=0;i<raw.length;i++) arr[i]=raw.charCodeAt(i); return new Blob([arr], {type:mime}); }catch(e){ return null; } }
function insertAtSel(node){const s=window.getSelection(); if(!s||!s.rangeCount){content.appendChild(node); return} const r=s.getRangeAt(0); r.deleteContents(); r.insertNode(node); r.setStartAfter(node); r.setEndAfter(node); s.removeAllRanges(); s.addRange(r)}
// Thumbnail via file chooser only
tFile.addEventListener('change',e=>{const f=e.target.files[0]; if(f) fileToDataURL(f).then(u=>tPrev.src=u)});
tDrop.addEventListener('click',()=>tFile.click());
content.addEventListener('paste',e=>{const it=(e.clipboardData||{}).items||[]; for(const x of it){const f=x.getAsFile&&x.getAsFile(); if(!f) continue; e.preventDefault(); if(f.type.startsWith('image/')) {
// Insert a placeholder while uploading
const ph=imgEl(''); ph.alt='image'; ph.style.minHeight='120px'; ph.style.background='#f0f0f0'; insertAtSel(ph);
(async()=>{ const url=await uploadBlobAndGetURL(f); if(url){ ph.removeAttribute('style'); ph.src=url; } else { fileToDataURL(f).then(u=>{ ph.removeAttribute('style'); ph.src=u; }); } })();
} else if(f.type.startsWith('video/')) {
fileToDataURL(f).then(u=>{const v=videoEl(u); insertAtSel(v)});
} } });
// Insert buttons
insImg.onclick=()=>{ imgFile.onchange=async e=>{ const f=e.target.files&&e.target.files[0]; if(!f) return; const ph=imgEl(''); ph.alt='image'; ph.style.minHeight='120px'; ph.style.background='#f0f0f0'; insertAtSel(ph); const url=await uploadBlobAndGetURL(f); if(url){ ph.removeAttribute('style'); ph.src=url; } else { fileToDataURL(f).then(u=>{ ph.removeAttribute('style'); ph.src=u; }); } }; imgFile.click(); };
insVid.onclick=()=>pick(vidFile, u=>{const v=videoEl(u); insertAtSel(v)});
// Save article
saveBtn.onclick=async()=>{
// Remove any empty placeholder images prior to saving
content.querySelectorAll('img[src=""], img:not([src])').forEach(n=>n.remove());
let th=tPrev.src||''; if(!th){const fi=content.querySelector('img'); if(fi) th=fi.src}
// If thumbnail is data URL, try uploading to separate file so it loads separately
if(th && th.startsWith('data:')){ const blob=dataURLToBlob(th); if(blob){ const u=await uploadBlobAndGetURL(blob); if(u) th=u; } }
const obj={title:title.value.trim()||'Untitled', author:author.value.trim(), body:content.innerHTML, thumb:th||''};
setLoading(true,'Saving…');
await API.create(obj);
selId=null; clearEditor();
await renderList();
setLoading(false);
show(listV);
};
function clearEditor(){ title.value=''; author.value=''; content.innerHTML=''; tPrev.removeAttribute('src') }
cancelBtn.onclick=()=>{selId=null; clearEditor(); show(listV)};
// List click -> read (ensure full focus by switching view)
listV.addEventListener('click',async e=>{const card=e.target.closest('.card'); if(!card) return; const id=card.getAttribute('data-id'); const c=AC[id]; if(c){ openRead(c); return; } setLoading(true,'Loading article…'); try{ const a=await API.get(id); if(!a) return; AC[id]=a; openRead(a); } finally { setLoading(false); }});
function openRead(a){ selId=a.id; rTh.loading='lazy'; rTh.decoding='async'; rTh.src=a.thumb||''; rT.textContent=a.title||''; rB.innerHTML=a.body||''; rB.querySelectorAll('img').forEach(im=>{ im.loading='lazy'; im.decoding='async'; }); const d=new Date(a.createdAt||Date.now()).toLocaleString(); dt.textContent=(a.author? a.author+' · ': '')+d; show(readV) }
// Delete
del.onclick=async()=>{ if(!selId) return; setLoading(true,'Deleting…'); await API.remove(selId); delete AC[selId]; selId=null; await renderList(); setLoading(false); show(listV) };
// Nav
toList.onclick=async()=>{ setLoading(true,'Loading articles…'); await renderList(); setLoading(false); show(listV)};
toNew.onclick=()=>{selId=null; clearEditor(); show(editor)};
back.onclick=()=>{show(listV)};
// No deep linking; keep UI simple and focused
(async function init(){
setLoading(true,'Loading articles…');
API=await detectAPI();
let items;
if(typeof preList!== 'undefined'){ items = preList; preList = undefined; }
else { items = await API.list(); }
if(!items.length){
const created = await API.create({title:'Welcome to Mini Articles', body:'<p>Edit or create your first article. Drop images or videos right into the text.</p>', thumb:''});
items = [created];
}
await renderList(items);
show(listV);
setLoading(false);
})();
navigator.serviceWorker&&navigator.serviceWorker.register('/sw.js').catch(()=>{});
})();
</script>
</html>

33
python_pkg/articles/run.sh Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
# Run Mini Articles (backend + frontend) using the C server only
# Options (env): HOST (default 127.0.0.1), PORT (default 8000), ARTICLES_DATA_DIR
DIR=$(cd -- "$(dirname -- "$0")" && pwd)
SITE_DIR="$DIR"
HOST="${HOST:-127.0.0.1}"
PORT="${PORT:-8000}"
make -s -C "$SITE_DIR" server_c
# Start C server in background
export HOST PORT ARTICLES_DATA_DIR
"$SITE_DIR/server_c" &
SRV_PID=$!
trap 'kill $SRV_PID 2>/dev/null || true' EXIT INT TERM
# Give it a moment to start
sleep 0.5
URL="http://$HOST:$PORT/"
# Try to open browser on Linux
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$URL" >/dev/null 2>&1 || true
fi
echo "Mini Articles running at $URL"
echo "Press Ctrl+C to stop."
# Wait on server
wait "$SRV_PID"

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
# Run only the website tests from this directory
DIR=$(cd -- "$(dirname -- "$0")" && pwd)
cd "$DIR"
PYTHON_BIN="${PYTHON:-}"
if [[ -z "${PYTHON_BIN}" ]]; then
if command -v python >/dev/null 2>&1; then PYTHON_BIN=python
elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN=python3
else
echo "Python is required but not found in PATH." >&2
exit 1
fi
fi
# Be explicit to avoid collecting tests from other repo paths
"$PYTHON_BIN" -m pytest -q test_site_size.py test_server_api.py

File diff suppressed because it is too large Load Diff

31
python_pkg/articles/sw.js Normal file
View File

@ -0,0 +1,31 @@
// Minimal image cache-first service worker
const C = 'articles-img-v2';
const AC = 'articles-json-v1';
self.addEventListener('install', e => self.skipWaiting());
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
self.addEventListener('fetch', e => {
const req = e.request;
if (req.method !== 'GET') return;
const u = new URL(req.url);
const isImg = req.destination === 'image' || u.pathname.startsWith('/uploads/');
const isArticle = u.pathname.startsWith('/api/articles/') && u.pathname.length > '/api/articles/'.length;
if (isImg) {
e.respondWith((async () => {
const cache = await caches.open(C);
const hit = await cache.match(req, { ignoreVary: true, ignoreSearch: false });
if (hit) return hit;
const res = await fetch(req);
if (res && res.ok) cache.put(req, res.clone());
return res;
})());
} else if (isArticle) {
e.respondWith((async () => {
const cache = await caches.open(AC);
const hit = await cache.match(req);
if (hit) return hit;
const res = await fetch(req);
if (res && res.ok) cache.put(req, res.clone());
return res;
})());
}
});

View File

@ -0,0 +1,109 @@
"""Integration tests for the articles C server API."""
from http import HTTPStatus
import json
import os
from pathlib import Path
import socket
import subprocess
import time
from typing import Any
import urllib.error
import urllib.request
import pytest
def _req(
url: str, method: str = "GET", data: dict[str, Any] | bytes | None = None
) -> tuple[int, bytes]:
"""Send an HTTP request and return status code and body."""
if data is not None and not isinstance(data, bytes | bytearray):
data = json.dumps(data).encode("utf-8")
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=5) as resp:
body = resp.read()
return resp.getcode(), body
def test_crud_roundtrip(tmp_path: Path) -> None:
"""Test full CRUD lifecycle for articles API."""
# Build C server
here = Path(__file__).resolve().parent
subprocess.run(["make", "-s", "server_c"], check=True, cwd=str(here))
# Find a free port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
_, port = s.getsockname()
host = "127.0.0.1"
base = f"http://{host}:{port}"
# Isolate storage and start server
env = os.environ.copy()
env["ARTICLES_DATA_DIR"] = str(tmp_path)
env["HOST"] = host
env["PORT"] = str(port)
srv = subprocess.Popen(["./server_c"], cwd=str(here), env=env)
try:
# wait briefly for server to be ready
for _ in range(30):
try:
with urllib.request.urlopen(
base + "/api/articles", timeout=0.2
) as resp:
resp.read()
break
except (OSError, urllib.error.URLError):
time.sleep(0.05)
# Create
code, body = _req(
base + "/api/articles",
method="POST",
data={
"title": "T1",
"body": "<p>Hello</p>",
"thumb": "data:image/png;base64,xyz",
},
)
assert code == HTTPStatus.CREATED
created = json.loads(body)
art_id = created["id"]
# List
code, body = _req(base + "/api/articles")
assert code == HTTPStatus.OK
items = json.loads(body)
assert any(a["id"] == art_id for a in items)
# Get one
code, body = _req(base + f"/api/articles/{art_id}")
assert code == HTTPStatus.OK
got = json.loads(body)
assert got["title"] == "T1"
# Update
code, body = _req(
base + f"/api/articles/{art_id}", method="PUT", data={"title": "T2"}
)
assert code == HTTPStatus.OK
updated = json.loads(body)
assert updated["title"] == "T2"
# Delete
code, _ = _req(base + f"/api/articles/{art_id}", method="DELETE")
assert code == HTTPStatus.NO_CONTENT
# Ensure gone
with pytest.raises(urllib.error.HTTPError) as exc_info:
_req(base + f"/api/articles/{art_id}")
assert exc_info.value.code == HTTPStatus.NOT_FOUND
finally:
srv.terminate()
try:
srv.wait(timeout=2)
except subprocess.TimeoutExpired:
srv.kill()

View File

@ -0,0 +1,20 @@
"""Tests to ensure website stays within size budget."""
from pathlib import Path
# Budget for the entire website (single file) in bytes
BUDGET = 14 * 1024 # 14 KiB
HERE = Path(__file__).parent
SITE_FILE = HERE / "index.html"
def test_site_file_exists() -> None:
"""Verify the main site HTML file exists."""
assert SITE_FILE.exists(), f"Missing site file: {SITE_FILE}"
def test_site_size_under_budget() -> None:
"""Verify site size is under the defined budget."""
size = SITE_FILE.stat().st_size
assert size <= BUDGET, f"Site size {size} bytes exceeds budget {BUDGET}"

View File

@ -0,0 +1,31 @@
BEGIN{ in_func=0; depth=0; start=0; err=0; prev="" }
{
line=$0
# track function start when we see an opening brace at top-level and previous non-empty
# line looks like a function signature (ends with ')' and not ';', and not a typedef/struct/enum/union)
for(i=1;i<=length(line);i++){
c=substr(line,i,1)
if(c=="{"){
if(depth==0 && !in_func){
# Heuristic check on previous non-empty trimmed line
t=prev
sub(/^\s+/, "", t); sub(/\s+$/, "", t)
if(t ~ /\)$/ && t !~ /;\s*$/ && t !~ /^(typedef|struct|enum|union)\b/){
in_func=1; start=NR
}
}
depth++
} else if(c=="}"){
depth--
if(in_func && depth==0){
lines=NR-start+1
if(lines>20){ print FILENAME ":" start " function too long: " lines " lines"; err=1 }
in_func=0
}
}
}
# update previous non-empty line
tmp=line; sub(/^\s+/, "", tmp); sub(/\s+$/, "", tmp)
if(length(tmp)>0){ prev=tmp }
}
END{ if(err) exit 1 }

View File

@ -0,0 +1 @@
"""MoviePy 2.x comprehensive showcase package."""

View File

@ -0,0 +1,357 @@
"""MoviePy showcase — Part 4 (Audio), 5 (Composition), 6 (Drawing), 7 (Output)."""
from __future__ import annotations
from moviepy import (
AudioArrayClip,
AudioClip,
ColorClip,
CompositeAudioClip,
CompositeVideoClip,
ImageClip,
TextClip,
VideoClip,
concatenate_audioclips,
concatenate_videoclips,
)
from moviepy.audio.fx import (
AudioDelay,
AudioFadeIn,
AudioFadeOut,
AudioLoop,
AudioNormalize,
MultiplyStereoVolume,
MultiplyVolume,
)
from moviepy.video.compositing.CompositeVideoClip import clips_array
from moviepy.video.tools.drawing import (
circle,
color_gradient,
color_split,
)
import numpy as np
from python_pkg.moviepy_showcase.moviepy_showcase import (
CLIP_DUR,
FONT_B,
FONT_R,
H,
W,
_base_clip,
_resize_to_canvas,
_section_header,
_titled,
)
def _make_sine(freq: float = 440.0, dur: float = CLIP_DUR) -> AudioClip:
"""Pure sine-wave AudioClip."""
def maker(t: np.ndarray) -> np.ndarray:
t_arr = np.asarray(t)
wave = 0.3 * np.sin(2 * np.pi * freq * t_arr.flatten())
stereo = np.column_stack([wave, wave])
# MoviePy probes with scalar t=0 and uses len(list(frame0))
# for nchannels. A (1,2) array iterates as 1 row → nchannels=1.
# Returning shape (2,) for scalar t lets MoviePy detect 2 channels.
if t_arr.ndim == 0:
return stereo[0]
return stereo
return AudioClip(maker, duration=dur, fps=44100)
def part4_audio() -> list[VideoClip]:
"""Demonstrate audio clips and all 7 audio effects."""
scenes: list[VideoClip] = [
_section_header(
"Part 4: Audio",
"AudioClip · AudioArrayClip · CompositeAudioClip · 7 Audio Effects",
),
]
bg = ColorClip(size=(W, H), color=(20, 30, 50))
# AudioClip
a1 = _make_sine(440, CLIP_DUR)
c1 = bg.with_duration(CLIP_DUR).with_audio(a1)
scenes.append(_titled(c1, "AudioClip(sine_440Hz)"))
# AudioArrayClip
sr = 44100
t_arr = np.linspace(0, CLIP_DUR, int(sr * CLIP_DUR), endpoint=False)
arr = (0.3 * np.sin(2 * np.pi * 880 * t_arr)).astype(np.float64)
stereo = np.column_stack([arr, arr])
a2 = AudioArrayClip(stereo, fps=sr)
c2 = bg.with_duration(CLIP_DUR).with_audio(a2)
scenes.append(_titled(c2, "AudioArrayClip(numpy_array, fps=44100) # 880Hz"))
# CompositeAudioClip
low = _make_sine(220, CLIP_DUR)
high = _make_sine(660, CLIP_DUR)
comp_audio = CompositeAudioClip([low, high])
c3 = bg.with_duration(CLIP_DUR).with_audio(comp_audio)
scenes.append(_titled(c3, "CompositeAudioClip([220Hz, 660Hz])"))
# concatenate_audioclips
a_cat = concatenate_audioclips([_make_sine(330, 1.0), _make_sine(550, 1.0)])
c4 = bg.with_duration(CLIP_DUR).with_audio(a_cat)
scenes.append(_titled(c4, "concatenate_audioclips([330Hz, 550Hz])"))
# AudioFadeIn
a_fi = _make_sine(440, CLIP_DUR).with_effects([AudioFadeIn(duration=1.5)])
c5 = bg.with_duration(CLIP_DUR).with_audio(a_fi)
scenes.append(_titled(c5, "AudioFadeIn(duration=1.5)"))
# AudioFadeOut
a_fo = _make_sine(440, CLIP_DUR).with_effects([AudioFadeOut(duration=1.5)])
c6 = bg.with_duration(CLIP_DUR).with_audio(a_fo)
scenes.append(_titled(c6, "AudioFadeOut(duration=1.5)"))
# AudioDelay
a_delay = _make_sine(440, CLIP_DUR).with_effects(
[AudioDelay(offset=0.2, n_repeats=4, decay=1)]
)
c7 = bg.with_duration(a_delay.duration).with_audio(a_delay)
scenes.append(
_titled(
c7.with_duration(CLIP_DUR), "AudioDelay(offset=0.2, n_repeats=4, decay=1)"
)
)
# AudioLoop
short_a = _make_sine(440, 0.5)
a_loop = short_a.with_effects([AudioLoop(duration=CLIP_DUR)])
c8 = bg.with_duration(CLIP_DUR).with_audio(a_loop)
scenes.append(_titled(c8, "AudioLoop(duration=2.0)"))
# AudioNormalize
quiet = _make_sine(440, CLIP_DUR) # already normalized but demonstrates the call
a_norm = quiet.with_effects([AudioNormalize()])
c9 = bg.with_duration(CLIP_DUR).with_audio(a_norm)
scenes.append(_titled(c9, "AudioNormalize()"))
# MultiplyStereoVolume
a_stereo = _make_sine(440, CLIP_DUR).with_effects(
[MultiplyStereoVolume(left=1.0, right=0.2)]
)
c10 = bg.with_duration(CLIP_DUR).with_audio(a_stereo)
scenes.append(_titled(c10, "MultiplyStereoVolume(left=1.0, right=0.2)"))
# MultiplyVolume
a_vol = _make_sine(440, CLIP_DUR).with_effects([MultiplyVolume(factor=0.3)])
c11 = bg.with_duration(CLIP_DUR).with_audio(a_vol)
scenes.append(_titled(c11, "MultiplyVolume(factor=0.3)"))
return scenes
def part5_composition() -> list[VideoClip]:
"""Demonstrate composition & concatenation."""
scenes: list[VideoClip] = [
_section_header(
"Part 5: Composition",
"CompositeVideoClip · concatenate_videoclips · clips_array",
),
]
# CompositeVideoClip with bg_color, use_bgclip
bg = _base_clip()
overlay = (
ColorClip(size=(400, 400), color=(255, 50, 50))
.with_duration(CLIP_DUR)
.with_position(("center", "center"))
.with_opacity(0.6)
)
comp1 = CompositeVideoClip([bg, overlay], size=(W, H), bg_color=(0, 0, 0))
scenes.append(_titled(comp1, "CompositeVideoClip(clips, bg_color, use_bgclip)"))
# concatenate_videoclips — method='chain'
c1 = ColorClip(size=(W, H), color=(200, 50, 50)).with_duration(0.7)
c2 = ColorClip(size=(W, H), color=(50, 200, 50)).with_duration(0.7)
c3 = ColorClip(size=(W, H), color=(50, 50, 200)).with_duration(0.6)
cat = concatenate_videoclips([c1, c2, c3], method="chain")
scenes.append(_titled(cat, "concatenate_videoclips(method='chain')"))
# concatenate_videoclips — method='compose' with padding
cat2 = concatenate_videoclips(
[
c1.resized((W // 2, H // 2)),
c2.resized((W // 2, H // 2)),
c3.resized((W, H)),
],
method="compose",
bg_color=(0, 0, 0),
padding=-0.2,
)
scenes.append(
_titled(
_resize_to_canvas(cat2),
"concatenate_videoclips(method='compose', padding=-0.2)",
)
)
# concatenate_videoclips with transition
cat3 = concatenate_videoclips(
[c1, c2, c3],
padding=-0.3,
method="compose",
)
scenes.append(
_titled(
cat3.with_duration(CLIP_DUR),
"concatenate_videoclips(padding=-0.3) # overlap",
)
)
# clips_array
a = ColorClip(size=(W // 2, H // 2), color=(200, 50, 50)).with_duration(CLIP_DUR)
b = ColorClip(size=(W // 2, H // 2), color=(50, 200, 50)).with_duration(CLIP_DUR)
c = ColorClip(size=(W // 2, H // 2), color=(50, 50, 200)).with_duration(CLIP_DUR)
d = ColorClip(size=(W // 2, H // 2), color=(200, 200, 50)).with_duration(CLIP_DUR)
grid = clips_array([[a, b], [c, d]])
scenes.append(_titled(_resize_to_canvas(grid), "clips_array([[a, b], [c, d]])"))
return scenes
def part6_drawing_tools() -> list[VideoClip]:
"""Demonstrate moviepy.video.tools.drawing functions."""
scenes: list[VideoClip] = [
_section_header(
"Part 6: Drawing Tools", "circle · color_gradient · color_split"
),
]
# circle
circ = circle(
screensize=(W, H),
center=(W // 2, H // 2),
radius=300,
color=1.0,
bg_color=0.0,
blur=30,
)
circ_rgb = (np.dstack([circ, circ, circ]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(circ_rgb, duration=CLIP_DUR),
"drawing.circle(center, radius=300, blur=30)",
)
)
# color_gradient — linear
grad = color_gradient(
size=(W, H),
p1=(0, 0),
p2=(W, H),
color_1=0.0,
color_2=1.0,
shape="linear",
)
grad_rgb = (np.dstack([grad, grad, grad]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(grad_rgb, duration=CLIP_DUR),
"drawing.color_gradient(shape='linear')",
)
)
# color_gradient — radial
grad_r = color_gradient(
size=(W, H),
p1=(W // 2, H // 2),
radius=500,
color_1=1.0,
color_2=0.0,
shape="radial",
)
grad_r_rgb = (np.dstack([grad_r, grad_r, grad_r]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(grad_r_rgb, duration=CLIP_DUR),
"drawing.color_gradient(shape='radial', radius=500)",
)
)
# color_split
split = color_split(
size=(W, H),
x=W // 2,
color_1=0.0,
color_2=1.0,
gradient_width=100,
)
split_rgb = (np.dstack([split, split, split]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(split_rgb, duration=CLIP_DUR),
"drawing.color_split(x=W/2, gradient_width=100)",
)
)
return scenes
def part7_output() -> list[VideoClip]:
"""Label-only slides for output methods + parameters."""
scenes: list[VideoClip] = [
_section_header(
"Part 7: Output Methods",
"write_videofile · write_gif · save_frame · write_images_sequence",
),
]
bg = ColorClip(size=(W, H), color=(15, 20, 35))
methods = [
(
"write_videofile()",
"filename, fps, codec, bitrate, audio, audio_fps,\n"
"preset, audio_nbytes, audio_codec, audio_bitrate,\n"
"audio_bufsize, temp_audiofile, threads,\n"
"ffmpeg_params, logger, pixel_format",
),
(
"write_gif()",
"filename, fps, loop, logger",
),
(
"save_frame()",
"filename, t, with_mask",
),
(
"write_images_sequence()",
"name_format, fps, with_mask, logger",
),
(
"write_audiofile()",
"filename, fps, nbytes, buffersize,\ncodec, bitrate, ffmpeg_params, logger",
),
]
for title, params in methods:
t1 = (
TextClip(
text=title, font_size=56, color="cyan", font=FONT_B, margin=(0, 20)
)
.with_duration(2.5)
.with_position(("center", 300))
)
t2 = (
TextClip(
text=f"Parameters:\n{params}",
font_size=32,
color="#dddddd",
font=FONT_R,
method="caption",
size=(W - 300, None),
text_align="center",
interline=8,
margin=(0, 15),
)
.with_duration(2.5)
.with_position(("center", 500))
)
scenes.append(CompositeVideoClip([bg.with_duration(2.5), t1, t2], size=(W, H)))
return scenes

View File

@ -0,0 +1,282 @@
"""MoviePy showcase — Part 1 (Clip Types) and Part 2 (Clip Methods)."""
from __future__ import annotations
from typing import TYPE_CHECKING
from moviepy import (
BitmapClip,
ColorClip,
CompositeVideoClip,
DataVideoClip,
ImageClip,
ImageSequenceClip,
TextClip,
VideoClip,
)
from moviepy.video.fx import InvertColors
from moviepy.video.tools.drawing import circle
import numpy as np
from python_pkg.moviepy_showcase.moviepy_showcase import (
CLIP_DUR,
FONT_B,
FONT_R,
FPS,
H,
W,
_base_clip,
_gradient,
_resize_to_canvas,
_section_header,
_titled,
)
if TYPE_CHECKING:
from collections.abc import Callable
def part1_clip_types() -> list[VideoClip]:
"""Demonstrate every clip creation class."""
scenes: list[VideoClip] = [
_section_header(
"Part 1: Clip Types",
"VideoClip · ColorClip · TextClip · ImageClip"
" · BitmapClip · DataVideoClip · ImageSequenceClip",
),
]
# 1. VideoClip — custom frame function
vc = VideoClip(_gradient, duration=CLIP_DUR).with_fps(FPS)
scenes.append(_titled(vc, "VideoClip(frame_function)"))
# 2. ColorClip
cc = ColorClip(size=(W, H), color=(0, 120, 200)).with_duration(CLIP_DUR)
scenes.append(_titled(cc, "ColorClip(size, color)"))
# 3. TextClip — label method
tbg = ColorClip(size=(W, H), color=(20, 20, 50)).with_duration(CLIP_DUR)
tc = (
TextClip(
text="Hello MoviePy!",
font_size=96,
color="yellow",
font=FONT_B,
stroke_color="black",
stroke_width=3,
bg_color=None,
margin=(10, 30),
method="label",
horizontal_align="center",
vertical_align="center",
transparent=True,
)
.with_duration(CLIP_DUR)
.with_position("center")
)
scenes.append(
_titled(
CompositeVideoClip([tbg, tc], size=(W, H)),
"TextClip(text, font_size, color, stroke, margin, method='label')",
)
)
# 4. TextClip — caption method (wraps text)
tbg2 = ColorClip(size=(W, H), color=(50, 20, 20)).with_duration(CLIP_DUR)
tc2 = (
TextClip(
text="This is a longer caption that wraps "
"because we use method='caption' with a fixed size.",
font_size=48,
color="white",
font=FONT_R,
method="caption",
size=(W - 200, None),
text_align="center",
interline=10,
margin=(0, 20),
)
.with_duration(CLIP_DUR)
.with_position("center")
)
scenes.append(
_titled(
CompositeVideoClip([tbg2, tc2], size=(W, H)),
"TextClip(method='caption', text_align, interline, size)",
)
)
# 5. ImageClip — from numpy array
img = np.zeros((H, W, 3), dtype=np.uint8)
img[200:880, 400:1520] = [255, 100, 50] # orange rectangle
ic = ImageClip(img, duration=CLIP_DUR)
scenes.append(_titled(ic, "ImageClip(numpy_array)"))
# 6. BitmapClip — from ASCII-art frames
frames = [
["RR__", "RR__", "__BB", "__BB"],
["__RR", "__RR", "BB__", "BB__"],
["RR__", "RR__", "__BB", "__BB"],
["__RR", "__RR", "BB__", "BB__"],
]
bc = BitmapClip(
frames,
fps=2,
color_dict={"R": (255, 0, 0), "B": (0, 0, 255), "_": (30, 30, 30)},
)
bc = _resize_to_canvas(bc)
scenes.append(
_titled(bc.with_duration(CLIP_DUR), "BitmapClip(bitmap_frames, color_dict)")
)
# 7. DataVideoClip — data-driven frames
data_list = list(range(60))
def data_to_frame(d: int) -> np.ndarray:
frame = np.full((H, W, 3), 30, dtype=np.uint8)
bar_w = int(d / 60 * (W - 100))
frame[400:680, 50 : 50 + bar_w] = [0, 200, 100]
return frame
dvc = DataVideoClip(data_list, data_to_frame, fps=FPS).with_duration(CLIP_DUR)
scenes.append(_titled(dvc, "DataVideoClip(data, data_to_frame)"))
# 8. ImageSequenceClip — from a list of arrays
seq_frames = []
for i in range(10):
f = np.full((H, W, 3), int(25 * i), dtype=np.uint8)
f[:, :, 0] = int(255 - 25 * i)
seq_frames.append(f)
isc = ImageSequenceClip(seq_frames, fps=5).with_duration(CLIP_DUR)
scenes.append(_titled(isc, "ImageSequenceClip(sequence, fps)"))
return scenes
def part2_clip_methods() -> list[VideoClip]:
"""Demonstrate VideoClip methods."""
scenes: list[VideoClip] = [
_section_header(
"Part 2: Clip Methods",
"subclipped · cropped · resized · rotated"
" · with_position · with_opacity · …",
),
]
base = _base_clip(3.0)
# subclipped
sc = base.subclipped(0.5, 2.5)
scenes.append(
_titled(_resize_to_canvas(sc), "subclipped(start_time=0.5, end_time=2.5)")
)
# cropped
cr = base.cropped(x1=200, y1=100, x2=1200, y2=700).with_duration(CLIP_DUR)
scenes.append(
_titled(_resize_to_canvas(cr), "cropped(x1=200, y1=100, x2=1200, y2=700)")
)
# resized — by factor
rs1 = base.resized(0.5).with_duration(CLIP_DUR)
scenes.append(_titled(_resize_to_canvas(rs1), "resized(0.5) # half size"))
# resized — by height
rs2 = base.resized(height=400).with_duration(CLIP_DUR)
scenes.append(_titled(_resize_to_canvas(rs2), "resized(height=400)"))
# rotated
rt = base.rotated(30, expand=False, bg_color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(_titled(rt, "rotated(angle=30, expand=False)"))
# with_position + with_opacity in a composite
small = base.resized(0.4).with_duration(CLIP_DUR)
bg = ColorClip(size=(W, H), color=(10, 10, 10)).with_duration(CLIP_DUR)
p1 = small.with_position((50, 50)).with_opacity(1.0)
p2 = small.with_position((500, 300)).with_opacity(0.5)
comp = CompositeVideoClip([bg, p1, p2], size=(W, H))
scenes.append(_titled(comp, "with_position() + with_opacity(0.5)"))
# with_mask — circular mask
mask_arr = circle(
screensize=(W, H),
center=(W // 2, H // 2),
radius=300,
color=1.0,
bg_color=0.0,
blur=20,
)
mask_clip = ImageClip(mask_arr, is_mask=True, duration=CLIP_DUR)
masked = base.with_duration(CLIP_DUR).with_mask(mask_clip)
mbg = ColorClip(size=(W, H), color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked], size=(W, H)),
"with_mask() — circular mask via drawing.circle()",
)
)
# image_transform
def flip_lr(img: np.ndarray) -> np.ndarray:
return img[:, ::-1]
it = base.image_transform(flip_lr).with_duration(CLIP_DUR)
scenes.append(_titled(it, "image_transform(flip_lr_func)"))
# transform
def shift_right(gf: Callable[[float], np.ndarray], t: float) -> np.ndarray:
frame = gf(t)
shift = int(t * 100)
return np.roll(frame, shift, axis=1)
tf = base.transform(shift_right).with_duration(CLIP_DUR)
scenes.append(_titled(tf, "transform(shift_right_func)"))
# time_transform
tt = base.time_transform(lambda t: t * 3).with_duration(CLIP_DUR)
scenes.append(_titled(tt, "time_transform(lambda t: t*3) # 3x speed"))
# with_speed_scaled
ss = base.with_speed_scaled(factor=0.5)
scenes.append(_titled(ss.with_duration(CLIP_DUR), "with_speed_scaled(factor=0.5)"))
# with_section_cut_out
sco = base.with_section_cut_out(0.5, 1.5)
scenes.append(
_titled(
sco.with_duration(min(sco.duration, CLIP_DUR)),
"with_section_cut_out(0.5, 1.5)",
)
)
# to_ImageClip
still = base.to_ImageClip(t=1.0, duration=CLIP_DUR)
scenes.append(_titled(still, "to_ImageClip(t=1.0) # freeze at t=1"))
# to_mask + to_RGB
bw = base.to_mask(canal=1).to_RGB().with_duration(CLIP_DUR)
scenes.append(_titled(bw, "to_mask(canal=1).to_RGB()"))
# with_background_color
small2 = base.resized(0.5).with_duration(CLIP_DUR)
wbg = small2.with_background_color(size=(W, H), color=(80, 0, 120))
scenes.append(_titled(wbg, "with_background_color(color=(80,0,120))"))
# with_effects_on_subclip
eos = base.with_effects_on_subclip(
[InvertColors()], start_time=0.5, end_time=1.5
).with_duration(CLIP_DUR)
scenes.append(_titled(eos, "with_effects_on_subclip([InvertColors], 0.5, 1.5)"))
# with_volume_scaled (visual label only — audio effect)
vsc = base.with_duration(CLIP_DUR)
scenes.append(_titled(vsc, "with_volume_scaled(factor) # scales audio amplitude"))
# with_layer_index
scenes.append(
_titled(
base.with_duration(CLIP_DUR), "with_layer_index(n) # compositing z-order"
)
)
return scenes

View File

@ -0,0 +1,336 @@
"""MoviePy showcase — Part 3 (all 34 Video Effects)."""
from __future__ import annotations
from moviepy import (
ColorClip,
CompositeVideoClip,
ImageClip,
VideoClip,
)
from moviepy.video.fx import (
AccelDecel,
BlackAndWhite,
Blink,
Crop,
CrossFadeIn,
CrossFadeOut,
EvenSize,
FadeIn,
FadeOut,
Freeze,
FreezeRegion,
GammaCorrection,
HeadBlur,
InvertColors,
Loop,
LumContrast,
MakeLoopable,
Margin,
MaskColor,
MirrorX,
MirrorY,
MultiplyColor,
MultiplySpeed,
Painting,
Resize,
Rotate,
Scroll,
SlideIn,
SlideOut,
SuperSample,
TimeMirror,
TimeSymmetrize,
)
from moviepy.video.tools.drawing import circle
import numpy as np
from python_pkg.moviepy_showcase.moviepy_showcase import (
CLIP_DUR,
H,
W,
_base_clip,
_resize_to_canvas,
_section_header,
_titled,
)
def _fx(effect: object, label: str, dur: float = CLIP_DUR) -> VideoClip:
"""Apply effect to base clip and label it."""
b = _base_clip(dur)
try:
result = b.with_effects([effect])
# Ensure it has a finite duration
if result.duration is None or result.duration <= 0:
result = result.with_duration(dur)
result = result.with_duration(min(result.duration, dur))
except (ValueError, OSError, AttributeError):
result = b
# Make sure it fits the canvas
if result.size != (W, H):
result = _resize_to_canvas(result)
return _titled(result, label)
def _part3_effects_1_to_17() -> list[VideoClip]:
"""Video effects 1-17: AccelDecel through MakeLoopable."""
scenes: list[VideoClip] = []
# 1. AccelDecel
scenes.append(
_fx(
AccelDecel(new_duration=CLIP_DUR, abruptness=2.0, soonness=1.0),
"AccelDecel(abruptness=2.0, soonness=1.0)",
)
)
# 2. BlackAndWhite
scenes.append(
_fx(
BlackAndWhite(preserve_luminosity=True),
"BlackAndWhite(preserve_luminosity=True)",
)
)
# 3. Blink
scenes.append(
_fx(
Blink(duration_on=0.3, duration_off=0.3),
"Blink(duration_on=0.3, duration_off=0.3)",
)
)
# 4. Crop
b_crop = _base_clip().with_effects([Crop(x1=200, y1=100, x2=1400, y2=800)])
scenes.append(
_titled(_resize_to_canvas(b_crop), "Crop(x1=200, y1=100, x2=1400, y2=800)")
)
# 5. CrossFadeIn
scenes.append(_fx(CrossFadeIn(duration=1.0), "CrossFadeIn(duration=1.0)"))
# 6. CrossFadeOut
scenes.append(_fx(CrossFadeOut(duration=1.0), "CrossFadeOut(duration=1.0)"))
# 7. EvenSize
scenes.append(_fx(EvenSize(), "EvenSize() # ensures even wxh"))
# 8. FadeIn
scenes.append(
_fx(
FadeIn(duration=1.5, initial_color=[0, 0, 0]),
"FadeIn(duration=1.5, initial_color=[0,0,0])",
)
)
# 9. FadeOut
scenes.append(
_fx(
FadeOut(duration=1.5, final_color=[0, 0, 0]),
"FadeOut(duration=1.5, final_color=[0,0,0])",
)
)
# 10. Freeze
scenes.append(
_fx(
Freeze(t=0.5, freeze_duration=1.0),
"Freeze(t=0.5, freeze_duration=1.0)",
dur=3.0,
)
)
# 11. FreezeRegion
scenes.append(
_fx(
FreezeRegion(t=0.5, region=(200, 100, 1400, 700)),
"FreezeRegion(t=0.5, region=(200,100,1400,700))",
)
)
# 12. GammaCorrection
scenes.append(_fx(GammaCorrection(gamma=2.5), "GammaCorrection(gamma=2.5)"))
# 13. HeadBlur
scenes.append(
_fx(
HeadBlur(
fx=lambda _: W // 2,
fy=lambda _: H // 2,
radius=100,
intensity=None,
),
"HeadBlur(fx, fy, radius=100)",
)
)
# 14. InvertColors
scenes.append(_fx(InvertColors(), "InvertColors()"))
# 15. Loop
short = _base_clip(0.5)
looped = short.with_effects([Loop(n=4)])
scenes.append(_titled(looped.with_duration(CLIP_DUR), "Loop(n=4)"))
# 16. LumContrast
scenes.append(
_fx(
LumContrast(lum=30, contrast=50, contrast_threshold=127),
"LumContrast(lum=30, contrast=50)",
)
)
# 17. MakeLoopable
scenes.append(
_fx(MakeLoopable(overlap_duration=0.5), "MakeLoopable(overlap_duration=0.5)")
)
return scenes
def _part3_effects_18_to_34() -> list[VideoClip]:
"""Video effects 18-34: Margin through TimeSymmetrize."""
scenes: list[VideoClip] = []
# 18. Margin
b_margin = _base_clip().with_effects(
[
Resize(0.7),
Margin(
margin_size=None,
left=40,
right=40,
top=20,
bottom=20,
color=(255, 0, 0),
opacity=1.0,
),
]
)
scenes.append(
_titled(
_resize_to_canvas(b_margin),
"Margin(left=40, right=40, top=20, bottom=20, color=red)",
)
)
# 19. MaskColor
scenes.append(
_fx(
MaskColor(color=(128, 128, 128), threshold=80, stiffness=1),
"MaskColor(color, threshold=80)",
)
)
# 20. MasksAnd
mask1 = circle((W, H), (W // 3, H // 2), 300, 1.0, 0.0, 1)
mask2 = circle((W, H), (2 * W // 3, H // 2), 300, 1.0, 0.0, 1)
combined = np.minimum(mask1, mask2)
m_clip = ImageClip(combined, is_mask=True, duration=CLIP_DUR)
masked_and = _base_clip().with_mask(m_clip)
mbg = ColorClip(size=(W, H), color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked_and], size=(W, H)),
"MasksAnd — intersection of two circle masks",
)
)
# 21. MasksOr
combined_or = np.maximum(mask1, mask2)
m_clip2 = ImageClip(combined_or, is_mask=True, duration=CLIP_DUR)
masked_or = _base_clip().with_mask(m_clip2)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked_or], size=(W, H)),
"MasksOr — union of two circle masks",
)
)
# 22. MirrorX
scenes.append(_fx(MirrorX(), "MirrorX() # horizontal flip"))
# 23. MirrorY
scenes.append(_fx(MirrorY(), "MirrorY() # vertical flip"))
# 24. MultiplyColor
scenes.append(_fx(MultiplyColor(factor=1.8), "MultiplyColor(factor=1.8)"))
# 25. MultiplySpeed
scenes.append(_fx(MultiplySpeed(factor=3.0), "MultiplySpeed(factor=3.0)", dur=4.0))
# 26. Painting
scenes.append(
_fx(
Painting(saturation=1.4, black=0.006),
"Painting(saturation=1.4, black=0.006)",
)
)
# 27. Resize
b_rs = _base_clip().with_effects([Resize(new_size=(960, 540))])
scenes.append(_titled(_resize_to_canvas(b_rs), "Resize(new_size=(960,540))"))
# 28. Rotate
scenes.append(
_fx(
Rotate(angle=45, expand=True, bg_color=(0, 0, 0)),
"Rotate(angle=45, expand=True)",
)
)
# 29. Scroll
# Draw bands
tall_arr = np.full((H * 3, W, 3), 40, dtype=np.uint8)
for i in range(6):
y0, y1 = i * H // 2, (i + 1) * H // 2
tall_arr[y0:y1, :] = [
(50 * i) % 256,
(100 + 30 * i) % 256,
(200 - 20 * i) % 256,
]
tall_clip = ImageClip(tall_arr, duration=CLIP_DUR).with_effects(
[
Scroll(h=H, y_speed=-300, w=W),
]
)
scenes.append(_titled(_resize_to_canvas(tall_clip), "Scroll(h, y_speed=-300)"))
# 30. SlideIn
si = _base_clip().with_effects([SlideIn(duration=1.0, side="left")])
scenes.append(_titled(si, "SlideIn(duration=1.0, side='left')"))
# 31. SlideOut
so = _base_clip().with_effects([SlideOut(duration=1.0, side="right")])
scenes.append(_titled(so, "SlideOut(duration=1.0, side='right')"))
# 32. SuperSample
scenes.append(_fx(SuperSample(d=0.1, n_frames=3), "SuperSample(d=0.1, n_frames=3)"))
# 33. TimeMirror
tm = _base_clip().with_effects([TimeMirror()])
scenes.append(
_titled(tm.with_duration(CLIP_DUR), "TimeMirror() # plays backwards")
)
# 34. TimeSymmetrize
ts = _base_clip().with_effects([TimeSymmetrize()])
scenes.append(
_titled(ts.with_duration(CLIP_DUR), "TimeSymmetrize() # forward then reverse")
)
return scenes
def part3_video_effects() -> list[VideoClip]:
"""Demonstrate all 34 video effects."""
scenes: list[VideoClip] = [
_section_header(
"Part 3: Video Effects",
"All 34 effects from moviepy.video.fx",
),
]
scenes.extend(_part3_effects_1_to_17())
scenes.extend(_part3_effects_18_to_34())
return scenes

View File

@ -0,0 +1,306 @@
"""MoviePy 2.x — Comprehensive Showcase of ALL Features.
Generates a video demonstrating every MoviePy class, method, effect,
and tool. Organised into sections:
Part 1: Clip Types (VideoClip, ColorClip, TextClip, ImageClip,
BitmapClip, DataVideoClip, ImageSequenceClip)
Part 2: Clip Methods (subclipped, cropped, resized, rotated, with_position,
with_opacity, with_mask, image_transform, transform,
time_transform, with_speed_scaled, with_section_cut_out,
to_ImageClip, to_mask, to_RGB, with_background_color,
with_effects_on_subclip, with_layer_index)
Part 3: Video Effects (all 34)
Part 4: Audio (AudioClip, AudioArrayClip, CompositeAudioClip,
all 7 audio effects)
Part 5: Composition (CompositeVideoClip, concatenate_videoclips, clips_array)
Part 6: Drawing Tools (circle, color_gradient, color_split)
Part 7: Output (write_videofile params, write_gif, save_frame,
write_images_sequence)
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
import shutil
import tempfile
from moviepy import (
ColorClip,
CompositeVideoClip,
TextClip,
VideoClip,
VideoFileClip,
concatenate_videoclips,
)
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
logger = logging.getLogger(__name__)
os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg"
# ── Constants ─────────────────────────────────────────────────────
W, H = 1920, 1080
FPS = 30
CLIP_DUR = 2.0 # duration of each demo clip
HEADER_DUR = 1.5 # duration of section headers
OUTPUT = "moviepy_showcase_full.mp4"
FONT_B = "/usr/share/fonts/noto/NotoSans-Bold.ttf"
FONT_R = "/usr/share/fonts/noto/NotoSans-Regular.ttf"
# ── Pre-computed gradient LUTs ────────────────────────────────────
_G_CH = (
np.linspace(0, 255, H, dtype=np.uint8)[:, None]
* np.ones(W, dtype=np.uint8)[None, :]
)
_B_CH = (
np.ones(H, dtype=np.uint8)[:, None]
* np.linspace(0, 255, W, dtype=np.uint8)[None, :]
)
def _gradient(t: float) -> np.ndarray:
f = np.empty((H, W, 3), dtype=np.uint8)
f[:, :, 0] = int(128 + 127 * np.sin(t * 2))
f[:, :, 1] = _G_CH
f[:, :, 2] = _B_CH
return f
def _checkerboard(t: float) -> np.ndarray:
sq = 60
off = int(t * 40) % sq
xs = np.arange(W, dtype=np.int32)[None, :]
ys = np.arange(H, dtype=np.int32)[:, None]
v = (((xs + off) // sq + (ys + off) // sq) % 2 * 255).astype(np.uint8)
return np.dstack([v, v, v])
# ── Helpers ───────────────────────────────────────────────────────
def _base_clip(dur: float = CLIP_DUR) -> VideoClip:
"""Animated gradient as a reusable base clip."""
return VideoClip(_gradient, duration=dur).with_fps(FPS)
def _label(
text: str,
size: int = 36,
color: str = "white",
pos: tuple[str, int] | tuple[str, str] = ("center", 40),
dur: float = CLIP_DUR,
) -> TextClip:
"""Small label overlay (transparent bg)."""
return (
TextClip(
text=text,
font_size=size,
color=color,
font=FONT_R,
margin=(0, 15),
)
.with_duration(dur)
.with_position(pos)
)
def _titled(clip: VideoClip, text: str) -> CompositeVideoClip:
"""Overlay a label onto a clip."""
lbl = _label(text, dur=clip.duration)
return CompositeVideoClip(
[clip.with_duration(clip.duration), lbl],
size=(W, H),
)
def _section_header(title: str, subtitle: str = "") -> CompositeVideoClip:
"""Dark background with centred title text."""
bg = ColorClip(size=(W, H), color=(15, 15, 40)).with_duration(HEADER_DUR)
t = (
TextClip(
text=title,
font_size=72,
color="white",
font=FONT_B,
margin=(0, 30),
)
.with_duration(HEADER_DUR)
.with_position(("center", 380))
)
parts: list[VideoClip] = [bg, t]
if subtitle:
s = (
TextClip(
text=subtitle,
font_size=32,
color="#aaaaaa",
font=FONT_R,
margin=(0, 15),
)
.with_duration(HEADER_DUR)
.with_position(("center", 520))
)
parts.append(s)
return CompositeVideoClip(parts, size=(W, H))
def _resize_to_canvas(clip: VideoClip) -> VideoClip:
"""Resize a clip to fit (W, H) and centre on black background."""
cw, ch = clip.size
scale = min(W / cw, H / ch)
return clip.resized(
width=int(cw * scale), height=int(ch * scale)
).with_background_color(size=(W, H), color=(0, 0, 0))
# ══════════════════════════════════════════════════════════════════
# ASSEMBLY — memory-safe: render each part to a temp file, then
# concatenate via VideoFileClip so only one part is in RAM at a time.
# ══════════════════════════════════════════════════════════════════
def _render_part(
scenes: list[VideoClip],
path: str,
label: str,
) -> None:
"""Concatenate *scenes*, write to *path*, then close all clips."""
logger.info(" Rendering %s (%d scenes) → %s", label, len(scenes), Path(path).name)
part = concatenate_videoclips(scenes, method="compose", bg_color=(0, 0, 0))
part.write_videofile(
path,
fps=FPS,
codec="libx264",
preset="ultrafast",
audio=False,
logger=None,
)
# Free memory
part.close()
for s in scenes:
s.close()
def main() -> None:
"""Assemble all parts into the final showcase video."""
logging.basicConfig(level=logging.INFO)
logger.info("Building MoviePy comprehensive showcase…")
tmpdir = tempfile.mkdtemp(prefix="moviepy_showcase_")
try:
_build(tmpdir)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def _build(tmpdir: str) -> None:
# ── Lazy imports of moved part builders ───────────────────────
from moviepy.audio.fx import MultiplyVolume
from python_pkg.moviepy_showcase._moviepy_audio_output import (
_make_sine,
part4_audio,
part5_composition,
part6_drawing_tools,
part7_output,
)
from python_pkg.moviepy_showcase._moviepy_clip_types import (
part1_clip_types,
part2_clip_methods,
)
from python_pkg.moviepy_showcase._moviepy_video_effects import part3_video_effects
# ── Render each part to its own temp file ─────────────────────
# Title card
title_bg = ColorClip(size=(W, H), color=(10, 10, 30)).with_duration(3.0)
title_txt = (
TextClip(
text="MoviePy 2.x\nComplete Feature Showcase",
font_size=80,
color="white",
font=FONT_B,
method="caption",
size=(W - 200, None),
text_align="center",
margin=(0, 40),
)
.with_duration(3.0)
.with_position("center")
)
title_card = CompositeVideoClip([title_bg, title_txt], size=(W, H)).with_effects(
[FadeIn(1.0)]
)
# Outro
outro_bg = ColorClip(size=(W, H), color=(10, 10, 30)).with_duration(3.0)
outro_txt = (
TextClip(
text="That's all of MoviePy 2.x!\n34 video effects · 7 audio effects\n"
"11 clip types · drawing tools · composition",
font_size=52,
color="white",
font=FONT_B,
method="caption",
size=(W - 200, None),
text_align="center",
margin=(0, 30),
)
.with_duration(3.0)
.with_position("center")
)
outro = CompositeVideoClip([outro_bg, outro_txt], size=(W, H)).with_effects(
[FadeOut(1.5)]
)
part_builders = [
("title", lambda: [title_card]),
("Part 1: Clip Types", part1_clip_types),
("Part 2: Clip Methods", part2_clip_methods),
("Part 3: Video Effects", part3_video_effects),
("Part 4: Audio", part4_audio),
("Part 5: Composition", part5_composition),
("Part 6: Drawing Tools", part6_drawing_tools),
("Part 7: Output Methods", part7_output),
("outro", lambda: [outro]),
]
part_files: list[str] = []
for i, (label, builder) in enumerate(part_builders):
path = str(Path(tmpdir) / f"part_{i:02d}.mp4")
scenes = builder()
_render_part(scenes, path, label)
part_files.append(path)
# ── Load temp files as lightweight VideoFileClips & concat ─────
logger.info("Concatenating all parts…")
file_clips = [VideoFileClip(p) for p in part_files]
final = concatenate_videoclips(file_clips, method="chain")
# Background audio
audio = _make_sine(330, final.duration).with_effects([MultiplyVolume(factor=0.5)])
final = final.with_audio(audio)
logger.info("Total duration: %.1fs", final.duration)
logger.info("Writing %s (NVENC GPU)…", OUTPUT)
final.write_videofile(
OUTPUT,
fps=FPS,
codec="h264_nvenc",
audio_codec="aac",
threads=os.cpu_count(),
ffmpeg_params=["-preset", "p4", "-rc", "constqp", "-qp", "18", "-b:v", "0"],
logger="bar",
)
# Clean up
final.close()
for c in file_clips:
c.close()
size_mb = Path(OUTPUT).stat().st_size / (1024 * 1024)
logger.info("✔ Saved %s (%.1f MB)", OUTPUT, size_mb)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,58 @@
# Texas Hold'em Modifier App
A fun web application that randomly applies modifiers to Texas Hold'em poker games with configurable probability.
## Features
- **Configurable Probability**: Adjust the chance of getting a modifier (0-100%)
- **15 Unique Modifiers**: Various game-changing rules like "High Stakes", "Wild Card", "Reverse Psychology", etc.
- **Statistics Tracking**: Keep track of rounds played and modifiers applied
- **Beautiful UI**: Modern, responsive design with poker-themed styling
- **Smooth Animations**: Visual feedback for button clicks and result displays
## How to Use
1. Open `index.html` in your web browser
2. Adjust the "Modifier Probability" slider to set the chance of getting a modifier
3. Click "Start Round" to begin a new round
4. The app will randomly decide whether to apply a modifier based on your probability setting
5. If a modifier is chosen, a random modifier will be selected and displayed
## Modifiers Included
- **High Stakes**: All bets are doubled
- **Wild Card**: Next card can be used as any card
- **Bluff Master**: See one opponent's card before betting
- **All-In Fever**: If someone goes all-in, everyone must match or fold
- **Lucky Sevens**: Any hand with a 7 beats a pair
- **Reverse Psychology**: Lowest hand wins
- **Split Pot**: Pot split between top 2 hands
- **Texas Twister**: Each player gets an extra hole card
- **Blind Luck**: Play blind until the river
- **Community Boost**: Extra community card revealed
- **Minimum Madness**: Minimum bet tripled
- **Suit Supremacy**: Random suit cards worth +1 rank
- **Quick Draw**: Betting time cut in half
- **Royal Treatment**: Face cards worth double
- **Chip Challenge**: Winner gets extra house chips
## Files
- `index.html`: Main HTML structure
- `style.css`: Styling and responsive design
- `script.js`: JavaScript functionality and modifier logic
## Customization
You can easily add new modifiers by using the `addModifier()` method:
```javascript
window.pokerApp.addModifier(
"Your Modifier Name",
"Description of what it does",
);
```
## Browser Compatibility
Works in all modern web browsers (Chrome, Firefox, Safari, Edge).

View File

@ -0,0 +1,138 @@
# Texas Hold'em Modifier App - Python Version
A desktop application built with Python and tkinter that randomly applies modifiers to Texas Hold'em poker games with configurable probability.
## Requirements
- Python 3.6+
- tkinter (usually comes with Python)
## How to Run
```bash
python poker_modifier_app.py
```
## Features
- **Configurable Probability**: Adjust the chance of getting a modifier (0-100%) with a slider
- **50+ Poker & Drinking Modifiers**: Real poker variations with drinking game twists!
- **Statistics Tracking**: Keep track of rounds played and modifiers applied
- **Modern GUI**: Clean, poker-themed interface with visual feedback
- **Easy to Extend**: Simple methods to add new modifiers
## How to Use
1. Run the Python script
2. Adjust the "Modifier Probability" slider to set the chance of getting a modifier
3. Click "Start Round" to begin a new round
4. The app will randomly decide whether to apply a modifier based on your probability setting
5. If a modifier is chosen, a random modifier will be selected and displayed
## Modifiers Included
### Classic Poker Modifiers
- **High Stakes**: All bets are doubled
- **Wild Card**: Next card can be used as any card
- **Bluff Master**: See one opponent's card before betting
- **All-In Fever**: If someone goes all-in, everyone must match or fold
- **Lucky Sevens**: Any hand with a 7 beats a pair
- **Reverse Psychology**: Lowest hand wins
- **Split Pot**: Pot split between top 2 hands
- **Texas Twister**: Each player gets an extra hole card
- **Blind Luck**: Play blind until the river
- **Community Boost**: Extra community card revealed
- **Minimum Madness**: Minimum bet tripled
- **Suit Supremacy**: Random suit cards worth +1 rank
- **Quick Draw**: Betting time cut in half
- **Royal Treatment**: Face cards worth double
- **Chip Challenge**: Winner gets extra house chips
## Modifiers Included
### Classic Poker Modifiers
- **High Stakes**: All bets are doubled
- **Wild Card**: Next card can be used as any card
- **Bluff Master**: See one opponent's card before betting
- **All-In Fever**: If someone goes all-in, everyone must match or fold
- **Lucky Sevens**: Any hand with a 7 beats a pair
- **Reverse Psychology**: Lowest hand wins
- **Split Pot**: Pot split between top 2 hands
- **Texas Twister**: Each player gets an extra hole card
- **Blind Luck**: Play blind until the river
- **Community Boost**: Extra community card revealed
- **Minimum Madness**: Minimum bet tripled
- **Suit Supremacy**: Random suit cards worth +1 rank
- **Quick Draw**: Betting time cut in half
- **Royal Treatment**: Face cards worth double
- **Chip Challenge**: Winner gets extra house chips
### Drinking Game Modifiers
- **Red or Black**: Guess community card colors for double winnings
- **Pocket Rockets**: Pocket Aces trigger drinks for everyone else
- **Rainbow Flop**: 3-suit flop boosts flush draws
- **Suited Connectors**: Beat any pocket pair
- **Drink or Fold**: Choose to drink and stay in or fold
- **Shot Clock**: 10 seconds per decision or auto-fold
- **Double Down**: Pay double to see opponent's cards
- **Bad Beat Jackpot**: Losing with full house+ makes others drink
- **Chaser Round**: Previous loser gets bonus stack
- **Face Card Frenzy**: Each face card = take a sip
- **Burn Card Reveal**: Matching burn cards = drinks + chips
- **Pair Tax**: Pocket pairs cost extra or drink
- **Kicker Clash**: Lowest kicker in tie drinks
- **Color Blind**: Red cards +1, black cards -1
- **Sip and Tell**: Drink and honestly rate your hand
- **Last Call**: Final betting round, no more cards
- **Drink the River**: River helps you = others drink
- **Tipsy Tells**: Must make exaggerated expressions
- **House Rules**: Deuces wild but drink when used
- **Side Bet Madness**: Bet on what flop will contain
- **Fold Penalty**: Folders drink and sit out next hand
- **Straight Shooter**: Complete straight = pick someone to finish drink
- **Flush Rush**: First flush wins side pot from all
- **Ace High Drama**: Ace high wins double but finish drink
- **Bluff Check**: Caught bluffing = drink + penalty
- **Small Ball**: Only minimum bets allowed
- **Position Power**: Button sees everyone's first card
- **Community Chest**: 6 community cards total
- **Heads Up**: Only top 2 hands after flop continue
- **Dealer's Choice**: Dealer picks wild suits
- **Ante Up**: Double ante or take two drinks
- **Showdown Shuffle**: Simultaneous card reveal
- **Lucky Draw**: Extra card, choose best 2
- **Betting Blind**: First round before looking at cards
- **Chip and a Chair**: Short stack sees early community card
- **All Red**: Red cards boost hand level
- **Mississippi Stud**: Fold after flop for half bet back
## Code Structure
- `PokerModifierApp`: Main application class
- `setup_gui()`: Creates the tkinter interface
- `start_round()`: Main game logic for starting rounds
- `apply_random_modifier()`: Selects and displays a random modifier
- `show_no_modifier()`: Displays when no modifier is chosen
- `add_modifier()`: Method to add new modifiers
- `get_stats()`: Returns current statistics
## Customization
You can easily add new modifiers programmatically:
```python
app = PokerModifierApp()
app.add_modifier("Your Modifier Name", "Description of what it does")
app.run()
```
## GUI Components
- **Title**: Application header
- **Settings Panel**: Probability slider
- **Result Display**: Shows modifier or "no modifier" message
- **Start Button**: Triggers new round
- **Statistics**: Displays rounds played and modifiers applied

View File

@ -0,0 +1 @@
"""Poker modifier application package."""

View File

@ -0,0 +1,303 @@
"""GUI setup methods for the poker modifier application."""
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from python_pkg.poker_modifier_app.poker_modifier_app import PokerModifierApp
class PokerGuiMixin:
"""Mixin providing GUI setup methods for PokerModifierApp."""
self: PokerModifierApp
def setup_gui(self) -> None:
"""Create and configure the main GUI window."""
self._setup_main_window()
main_frame = self._create_main_frame()
self._create_title(main_frame)
self._create_settings_frame(main_frame)
self._create_result_display(main_frame)
self._create_buttons(main_frame)
self._create_statistics_frame(main_frame)
def _setup_main_window(self) -> None:
"""Initialize the main Tk window."""
self.root = tk.Tk()
self.root.title("🃏 Texas Hold'em Modifier")
self.root.geometry("650x750")
self.root.configure(bg="#0f4c3a")
self.root.resizable(True, True)
style = ttk.Style()
style.theme_use("clam")
def _create_main_frame(self) -> tk.Frame:
"""Create and return the main container frame."""
main_frame = tk.Frame(self.root, bg="#0f4c3a", padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
return main_frame
def _create_title(self, parent: tk.Frame) -> None:
"""Create the title label."""
title_label = tk.Label(
parent,
text="🃏 Texas Hold'em Modifier",
font=("Arial", 24, "bold"),
fg="#ffd700",
bg="#0f4c3a",
)
title_label.pack(pady=(0, 20))
def _create_settings_frame(self, parent: tk.Frame) -> None:
"""Create the settings frame.
Includes probability, debug, and game length controls.
"""
settings_frame = tk.LabelFrame(
parent,
text="Settings",
font=("Arial", 12, "bold"),
fg="#ffd700",
bg="#1a6b4d",
relief=tk.RIDGE,
bd=2,
)
settings_frame.pack(fill=tk.X, pady=(0, 20), padx=10, ipady=10)
self._create_probability_controls(settings_frame)
self._create_debug_controls(settings_frame)
self._create_length_controls(settings_frame)
def _create_probability_controls(self, parent: tk.Widget) -> None:
"""Create the probability slider and label."""
prob_frame = tk.Frame(parent, bg="#1a6b4d")
prob_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(
prob_frame,
text="Modifier Probability:",
font=("Arial", 11, "bold"),
fg="white",
bg="#1a6b4d",
).pack(side=tk.LEFT)
self.prob_var = tk.IntVar(value=30)
self.prob_scale = tk.Scale(
prob_frame,
from_=0,
to=100,
orient=tk.HORIZONTAL,
variable=self.prob_var,
command=self.update_prob_display,
bg="#1a6b4d",
fg="white",
highlightbackground="#1a6b4d",
troughcolor="#0f4c3a",
activebackground="#ffd700",
)
self.prob_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5))
self.prob_label = tk.Label(
prob_frame,
text="30%",
font=("Arial", 11, "bold"),
fg="#ffd700",
bg="#1a6b4d",
width=5,
)
self.prob_label.pack(side=tk.RIGHT)
def _create_debug_controls(self, parent: tk.Widget) -> None:
"""Create the debug mode checkbox and force endgame button."""
debug_frame = tk.Frame(parent, bg="#1a6b4d")
debug_frame.pack(fill=tk.X, padx=10, pady=5)
self.debug_var = tk.BooleanVar(value=False)
debug_check = tk.Checkbutton(
debug_frame,
text="Debug Mode",
variable=self.debug_var,
command=self.toggle_debug_mode,
bg="#1a6b4d",
fg="white",
selectcolor="#0f4c3a",
activebackground="#1a6b4d",
activeforeground="#ffd700",
font=("Arial", 10, "bold"),
)
debug_check.pack(side=tk.LEFT, padx=(0, 15))
self.force_endgame_button = tk.Button(
debug_frame,
text="Force Endgame",
command=self.toggle_force_endgame,
bg="#ff6b6b",
fg="white",
font=("Arial", 9, "bold"),
relief=tk.RAISED,
bd=2,
)
# Initially hidden
def _create_length_controls(self, parent: tk.Widget) -> None:
"""Create the game length slider and label."""
length_frame = tk.Frame(parent, bg="#1a6b4d")
length_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(
length_frame,
text="Total Game Rounds:",
font=("Arial", 11, "bold"),
fg="white",
bg="#1a6b4d",
).pack(side=tk.LEFT)
self.length_var = tk.IntVar(value=20)
self.length_scale = tk.Scale(
length_frame,
from_=5,
to=50,
orient=tk.HORIZONTAL,
variable=self.length_var,
command=self.update_length_display,
bg="#1a6b4d",
fg="white",
highlightbackground="#1a6b4d",
troughcolor="#0f4c3a",
activebackground="#ffd700",
)
self.length_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5))
self.length_label = tk.Label(
length_frame,
text="20",
font=("Arial", 11, "bold"),
fg="#ffd700",
bg="#1a6b4d",
width=5,
)
self.length_label.pack(side=tk.RIGHT)
def _create_result_display(self, parent: tk.Frame) -> None:
"""Create the result display frame."""
self.result_frame = tk.Frame(
parent, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150
)
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10)
self.result_frame.pack_propagate(False)
self.result_label = tk.Label(
self.result_frame,
text="Click 'Start Round' to begin!",
font=("Arial", 14),
fg="#cccccc",
bg="#2d2d2d",
wraplength=500,
justify=tk.CENTER,
)
self.result_label.pack(expand=True, fill=tk.BOTH, padx=20, pady=20)
def _create_buttons(self, parent: tk.Frame) -> None:
"""Create the start and reset buttons."""
button_frame = tk.Frame(parent, bg="#0f4c3a")
button_frame.pack(fill=tk.X, pady=(0, 20), padx=10)
self.start_button = tk.Button(
button_frame,
text="Start Round",
font=("Arial", 18, "bold"),
bg="#ffd700",
fg="#0f4c3a",
activebackground="#ffed4e",
activeforeground="#0f4c3a",
relief=tk.RAISED,
bd=3,
command=self.start_round,
cursor="hand2",
)
self.start_button.pack(
side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5)
)
self.reset_button = tk.Button(
button_frame,
text="Reset Game",
font=("Arial", 14, "bold"),
bg="#ff6b6b",
fg="white",
activebackground="#ff5252",
activeforeground="white",
relief=tk.RAISED,
bd=3,
command=self.reset_game,
cursor="hand2",
)
self.reset_button.pack(side=tk.RIGHT, ipady=10, padx=(5, 0))
def _create_statistics_frame(self, parent: tk.Frame) -> None:
"""Create the statistics display frame with rounds, modifiers, and phase."""
stats_frame = tk.Frame(parent, bg="#0f4c3a")
stats_frame.pack(fill=tk.X, padx=10)
# Rounds played
rounds_frame = tk.LabelFrame(
stats_frame,
text="Rounds Played",
font=("Arial", 10, "bold"),
fg="#cccccc",
bg="#1a6b4d",
relief=tk.RIDGE,
bd=2,
)
rounds_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 3))
self.rounds_label = tk.Label(
rounds_frame,
text="0",
font=("Arial", 20, "bold"),
fg="#ffd700",
bg="#1a6b4d",
)
self.rounds_label.pack(pady=10)
# Modifiers applied
mods_frame = tk.LabelFrame(
stats_frame,
text="Modifiers Applied",
font=("Arial", 10, "bold"),
fg="#cccccc",
bg="#1a6b4d",
relief=tk.RIDGE,
bd=2,
)
mods_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(3, 3))
self.mods_label = tk.Label(
mods_frame, text="0", font=("Arial", 20, "bold"), fg="#ffd700", bg="#1a6b4d"
)
self.mods_label.pack(pady=10)
# Game phase indicator
phase_frame = tk.LabelFrame(
stats_frame,
text="Game Phase",
font=("Arial", 10, "bold"),
fg="#cccccc",
bg="#1a6b4d",
relief=tk.RIDGE,
bd=2,
)
phase_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(3, 0))
self.phase_label = tk.Label(
phase_frame,
text="Early",
font=("Arial", 16, "bold"),
fg="#4CAF50",
bg="#1a6b4d",
)
self.phase_label.pack(pady=10)

View File

@ -0,0 +1,465 @@
"""Modifier data constants for the poker modifier application."""
from __future__ import annotations
from typing import TypeAlias
Modifier: TypeAlias = dict[str, str]
REGULAR_MODIFIERS: list[Modifier] = [
# Hand Bonus Modifiers (Balatro-inspired)
{
"name": "Pair Bonus",
"description": (
"Any pocket pair: everyone else pays you 1 chip, "
"even if you lose the hand."
),
},
{
"name": "Flush Fever",
"description": (
"Make a flush: collect 1 chip from each other player "
"(separate from main pot)."
),
},
{
"name": "Straight Shot",
"description": (
"Complete a straight: choose one player "
"to pay you half the current pot size."
),
},
{
"name": "Full House Party",
"description": (
"Make full house: everyone else pays 2 chips + takes 2 drinks."
),
},
{
"name": "High Card Hero",
"description": (
"Win with just high card: collect your normal winnings "
"+ 1 chip from each player."
),
},
# Card Enhancement Modifiers
{
"name": "Face Card Power",
"description": "All face cards (J, Q, K) count as Aces for this hand.",
},
{
"name": "Red Suit Boost",
"description": (
"Hearts and Diamonds are worth +1 rank (Jack becomes Queen, etc.)"
),
},
{
"name": "Black Magic",
"description": (
"Spades and Clubs can be used as any suit for straights/flushes."
),
},
{
"name": "Lucky Sevens",
"description": "All 7s become wild cards that can be any rank.",
},
{
"name": "Steel Cards",
"description": (
"Random rank chosen: {steel_rank}. "
"All {steel_rank}s beat everything this hand!"
),
},
# Ante-Based Effects (Clear Money Source)
{
"name": "Bonus Pool",
"description": (
"Everyone puts 2 chips in bonus pool. "
"First person to make any pair wins it all."
),
},
# Deck Manipulation (Balatro-style)
{
"name": "Deck Shuffle",
"description": (
"After dealing hole cards, shuffle deck " "and redeal all community cards."
),
},
{
"name": "Extra Draw",
"description": (
"Deal each player a 3rd hole card. Discard one before the flop."
),
},
{
"name": "Phantom Cards",
"description": (
"Deal 6 community cards, but randomly remove 1 before showdown."
),
},
# Special Betting Rules (Realistic Economics)
{
"name": "Escalation",
"description": (
"Each raise must be at least 2x the previous raise " "(not just matching)."
),
},
# Position and Action Modifiers
{
"name": "Button Bonus",
"description": "Dealer button acts last in ALL rounds",
},
{
"name": "Call Penalty",
"description": (
"Anyone who only calls (never raises) pays 1 chip penalty to pot."
),
},
# Information Warfare
{
"name": "Poker Face",
"description": (
"No talking, no expressions allowed. Pure silent poker this hand."
),
},
{
"name": "Truth or Consequences",
"description": (
"If asked 'good hand or bad hand?' "
"you must answer truthfully or pay penalty."
),
},
{
"name": "Open Book",
"description": "Everyone plays with one hole card face-up.",
},
# Drinking Game Integration
{
"name": "Liquid Courage",
"description": (
"Take a drink before betting to get chip bonus to all your bets."
),
},
{
"name": "Last Call",
"description": (
"Everyone must finish their current drink before the river card."
),
},
{
"name": "Shot Clock",
"description": "5 seconds to act or take a shot and auto-fold.",
},
{
"name": "Drink Tax",
"description": (
"Each red card in your final hand = one sip (reveal after play)."
),
},
# Wild and Chaos Effects
{
"name": "Joker's Wild",
"description": (
"All Jacks become completely wild - any suit, any rank you choose."
),
},
{
"name": "Suit Swap",
"description": "Hearts become Spades, Diamonds become Clubs this hand.",
},
{
"name": "Rank Revolution",
"description": "2s beat Aces this hand. All other ranks stay the same.",
},
{
"name": "Time Warp",
"description": (
"Play the hand completely backwards: showdown first, "
"then remove random cards from table!"
),
},
# Economic Effects (Clear Money Sources)
{
"name": "Poverty Mode",
"description": "All bets limited to 1 chip maximum this hand.",
},
{
"name": "High Roller",
"description": "Minimum bet is 5x the entry this hand.",
},
{
"name": "Charity Case",
"description": (
"Player with fewest chips gets their ante funded by richest player."
),
},
# Penalty-Based Modifiers (Clear Consequences)
{
"name": "Fold Tax",
"description": "Anyone who folds pays 5 chip to the pot immediately.",
},
{
"name": "Bluff Fine",
"description": "Get caught bluffing = pay 2 chips to next hand's pot.",
},
{
"name": "Speed Fine",
"description": ("Take longer than 10 seconds to act = pay 1 chip to pot."),
},
{
"name": "Talk Tax",
"description": ("Every word spoken during betting costs 1 chip to the pot."),
},
# Skill Challenges (With Clear Rewards/Penalties)
{
"name": "Memory Challenge",
"description": (
"Dealer names all community cards in order. "
"Success = collect 1 chip from each. "
"Fail = pay 1 chip to each."
),
},
{
"name": "Quick Draw",
"description": (
"Everyone pays 1 chip to quick-draw pot. "
"First to correctly announce their hand wins the pot."
),
},
{
"name": "Bluff Bonus",
"description": (
"Successfully bluff with 7-high or worse "
"= collect 2 chips from each other player."
),
},
{
"name": "Prediction Pool",
"description": (
"Everyone puts 1 chip in pool. "
"Guess the river card exactly = win the pool."
),
},
# Partnership Modifiers
{
"name": "Buddy System",
"description": (
"Each player chooses a partner. "
"Partners share fate - both win or both lose."
),
},
{
"name": "Duo Power",
"description": (
"Partners can combine their hole cards - "
"each player plays with 4 cards total."
),
},
{
"name": "Shared Vision",
"description": (
"Partners can show each other one hole card before betting starts."
),
},
{
"name": "Tag Team",
"description": (
"Partners alternate who plays each betting round "
"(pre-flop, flop, turn, river)."
),
},
{
"name": "Power Couple",
"description": (
"If both partners make it to showdown, they both get +1 chip bonus "
"from other players (revealed at end of round)."
),
},
]
ENDGAME_MODIFIERS: list[Modifier] = [
# Classic Endgame Modifiers
{
"name": "Final Boss",
"description": ("This is the last hand. Winner takes all remaining chips."),
},
{
"name": "Sudden Death",
"description": "Anyone who folds is eliminated from the game.",
},
{
"name": "Comeback Kid",
"description": (
"Player with the worst hand can't lose chips this round "
"(reveal at the end of round)."
),
},
{
"name": "Double or Nothing",
"description": (
"Winner gets double payout, but everyone else pays double penalty."
),
},
# High Stakes Endgame
{
"name": "All In Madness",
"description": (
"Everyone must go all-in. No calling, no folding allowed this hand."
),
},
{
"name": "Chip Volcano",
"description": (
"Everyone puts half their remaining chips in the center. "
"Winner takes the mountain."
),
},
{
"name": "Last Stand",
"description": (
"Player with fewest chips gets to act last in ALL betting rounds."
),
},
# Dramatic Reversals
{
"name": "Underdog Victory",
"description": ("Worst hand wins the pot instead of best hand this round."),
},
# Winner Takes All Variants
{
"name": "Crown Jewels",
"description": (
"Winner of this hand becomes the 'King' - "
"all other players pay tribute (2 chips each)."
),
},
{
"name": "Championship Belt",
"description": (
"Winner takes 75% of all chips on the table. "
"Remaining 25% goes for the second best."
),
},
# Elimination Mechanics
{
"name": "Battle Royale",
"description": "Lowest hand is eliminated. If tied, both eliminated.",
},
{
"name": "Survivor",
"description": (
"Only players who improve their hand from pre-flop to river "
"survive to next round."
),
},
# Time Pressure Endgame
{
"name": "Speed Round",
"description": ("3 seconds to act or auto-fold. No exceptions, no delays."),
},
{
"name": "Auction House",
"description": (
"Players bid chips to see each other's hole cards before betting."
),
},
{
"name": "Lightning Round",
"description": (
"Deal all 5 community cards at once. "
"Betting happens after each card revealed."
),
},
# Psychological Warfare
{
"name": "Confession Booth",
"description": (
"Each player must truthfully state " "their biggest bluff this session."
),
},
{
"name": "Truth Serum",
"description": (
"Everyone must honestly rate their hand 1-10 before any betting."
),
},
{
"name": "Poker Face Off",
"description": (
"Staring contest: losers must reveal one hole card to the table."
),
},
# Endgame Economics
{
"name": "Wealth Redistribution",
"description": (
"Before the hand, richest player gives 3 chips to poorest player."
),
},
{
"name": "Emergency Fund",
"description": (
"All players with less than 5 chips " "get emergency funding from the pot."
),
},
{
"name": "Final Ante",
"description": (
"Everyone must put in their last 2 chips "
"before seeing cards. No backing out."
),
},
# Apocalypse Modifiers
{
"name": "Nuclear Option",
"description": (
"Dealer burns the top 3 cards. " "Play with whatever's left in the deck."
),
},
{
"name": "Meteor Strike",
"description": ("Remove all face cards from the deck for this hand only."),
},
{
"name": "Solar Flare",
"description": "All suits become the same suit (dealer's choice).",
},
# Legacy Modifiers
{
"name": "Hall of Fame",
"description": (
"Winner's name gets written down as 'Champion of the Session'."
),
},
{
"name": "Legendary Hand",
"description": ("This hand will be retold as a story. Play like legends."),
},
{
"name": "Photo Finish",
"description": (
"Take a photo of the winning hand - " "it goes in the poker hall of fame."
),
},
# Chaos Theory
{
"name": "Butterfly Effect",
"description": (
"One random decision by dealer changes everything: "
"flip a coin for each community card to reverse it."
),
},
{
"name": "Time Paradox",
"description": (
"Play the hand twice with same cards. Best average result wins."
),
},
{
"name": "Multiverse",
"description": (
"Deal 2 separate boards. Players choose "
"which board to play after seeing both."
),
},
]

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Texas Hold'em Modifier App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>🃏 Texas Hold'em Modifier</h1>
<div class="game-area">
<div class="probability-settings">
<h3>Settings</h3>
<div class="setting">
<label for="modifierChance">Modifier Probability:</label>
<input type="range" id="modifierChance" min="0" max="100" value="30">
<span id="chanceValue">30%</span>
</div>
</div>
<div class="result-area">
<div id="resultDisplay" class="result-display">
<p>Click "Start Round" to begin!</p>
</div>
</div>
<button id="startRoundBtn" class="start-button">Start Round</button>
<div class="stats">
<div class="stat">
<span class="stat-label">Rounds Played:</span>
<span id="roundsCount">0</span>
</div>
<div class="stat">
<span class="stat-label">Modifiers Applied:</span>
<span id="modifiersCount">0</span>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@ -0,0 +1,266 @@
"""Texas Hold'em poker game modifier application."""
import logging
import secrets
import tkinter as tk
from python_pkg.poker_modifier_app._poker_gui import PokerGuiMixin
from python_pkg.poker_modifier_app._poker_modifiers import (
ENDGAME_MODIFIERS,
REGULAR_MODIFIERS,
)
_logger = logging.getLogger(__name__)
# Use cryptographically secure random number generator
_rng = secrets.SystemRandom()
class PokerModifierApp(PokerGuiMixin):
"""GUI application for poker game modifiers."""
def __init__(self) -> None:
"""Initialize the poker modifier app with default settings."""
self.modifiers = list(REGULAR_MODIFIERS)
self.endgame_modifiers = list(ENDGAME_MODIFIERS)
# Remove endgame modifiers from regular modifier list
endgame_modifier_names = [mod["name"] for mod in self.endgame_modifiers]
self.modifiers = [
mod for mod in self.modifiers if mod["name"] not in endgame_modifier_names
]
# Game state tracking
self.rounds_played = 0
self.modifiers_applied = 0
self.total_game_rounds = 20 # Default game length
self.endgame_threshold = 0.8 # Start endgame modifiers at 80% of total rounds
self.debug_mode = False
self.force_endgame = False
self.setup_gui()
def update_prob_display(self, value: str) -> None:
"""Update the probability percentage display."""
self.prob_label.config(text=f"{value}%")
def update_length_display(self, value: str) -> None:
"""Update the game length display."""
self.length_label.config(text=str(value))
self.total_game_rounds = int(value)
def toggle_debug_mode(self) -> None:
"""Toggle debug mode and show/hide debug controls."""
self.debug_mode = self.debug_var.get()
if self.debug_mode:
self.force_endgame_button.pack(side=tk.LEFT, padx=(0, 10))
_logger.debug("Debug mode enabled")
else:
self.force_endgame_button.pack_forget()
self.force_endgame = False
_logger.debug("Debug mode disabled")
def toggle_force_endgame(self) -> None:
"""Toggle forced endgame mode for testing."""
self.force_endgame = not self.force_endgame
if self.force_endgame:
self.force_endgame_button.config(text="Stop Force Endgame", bg="#4CAF50")
_logger.debug("Forcing endgame modifiers")
else:
self.force_endgame_button.config(text="Force Endgame", bg="#ff6b6b")
_logger.debug("Normal modifier selection restored")
def is_endgame(self) -> bool:
"""Determine if we're in endgame phase."""
if self.debug_mode and self.force_endgame:
return True
endgame_round = int(self.total_game_rounds * self.endgame_threshold)
return self.rounds_played >= endgame_round
def start_round(self) -> None:
"""Start a new poker round and determine if modifier should be applied."""
# Button animation effect
self.start_button.config(relief=tk.SUNKEN)
self.root.after(100, lambda: self.start_button.config(relief=tk.RAISED))
# Update round counter
self.rounds_played += 1
self.rounds_label.config(text=str(self.rounds_played))
# Update game phase indicator
self.update_phase_indicator()
# Get current probability
modifier_chance = self.prob_var.get()
# Determine if modifier should be applied
random_value = _rng.random() * 100
should_apply_modifier = random_value < modifier_chance
if should_apply_modifier:
self.apply_random_modifier()
else:
self.show_no_modifier()
def update_phase_indicator(self) -> None:
"""Update the game phase indicator based on current round."""
if self.is_endgame():
self.phase_label.config(text="Endgame", fg="#ff6b6b")
elif self.rounds_played >= self.total_game_rounds * 0.6:
self.phase_label.config(text="Late", fg="#ffa500")
elif self.rounds_played >= self.total_game_rounds * 0.3:
self.phase_label.config(text="Mid", fg="#ffeb3b")
else:
self.phase_label.config(text="Early", fg="#4CAF50")
def apply_random_modifier(self) -> None:
"""Apply a random modifier and update display."""
# Update modifier counter
self.modifiers_applied += 1
self.mods_label.config(text=str(self.modifiers_applied))
# Determine which modifier pool to use
if self.is_endgame():
modifier_pool = self.endgame_modifiers
modifier_type = "🏁 ENDGAME"
bg_color = "#4a2d2d" # Darker red for endgame
else:
modifier_pool = self.modifiers
modifier_type = "🎲"
bg_color = "#2d4a2d" # Green for normal
# Select random modifier from appropriate pool
selected_modifier = _rng.choice(modifier_pool).copy()
# Special handling for Steel Cards - randomize the rank
if selected_modifier["name"] == "Steel Cards":
ranks = [
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"Jack",
"Queen",
"King",
"Ace",
]
steel_rank = _rng.choice(ranks)
selected_modifier["description"] = selected_modifier["description"].format(
steel_rank=steel_rank
)
# Update result frame styling for modifier
self.result_frame.config(
bg=bg_color, highlightbackground="#ffd700", highlightthickness=2
)
# Update display with modifier info
modifier_text = (
f"{modifier_type} {selected_modifier['name']}\n\n"
f"{selected_modifier['description']}"
)
# Add endgame indicator if applicable
if self.is_endgame():
rounds_left = self.total_game_rounds - self.rounds_played
if rounds_left > 0:
modifier_text += f"\n\n⚠️ Endgame Phase - {rounds_left} rounds left"
else:
modifier_text += "\n\n⚠️ FINAL ROUND!"
self.result_label.config(
text=modifier_text, fg="#ffd700", bg=bg_color, font=("Arial", 14, "bold")
)
def show_no_modifier(self) -> None:
"""Show no modifier message."""
# Update result frame styling for no modifier
self.result_frame.config(
bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1
)
# Update display
self.result_label.config(
text="No modifier this round\n\nPlay normally",
fg="#cccccc",
bg="#2d2d2d",
font=("Arial", 14),
)
def reset_game(self) -> None:
"""Reset the game to initial state."""
self.rounds_played = 0
self.modifiers_applied = 0
self.force_endgame = False
# Update displays
self.rounds_label.config(text="0")
self.mods_label.config(text="0")
self.phase_label.config(text="Early", fg="#4CAF50")
# Reset result frame
self.result_frame.config(
bg="#2d2d2d", highlightbackground="#666666", highlightthickness=1
)
self.result_label.config(
text="Click 'Start Round' to begin!",
fg="#cccccc",
bg="#2d2d2d",
font=("Arial", 14),
)
# Reset force endgame button if visible
if self.debug_mode:
self.force_endgame_button.config(text="Force Endgame", bg="#ff6b6b")
_logger.info("Game reset to initial state")
def add_modifier(self, name: str, description: str) -> None:
"""Add a new modifier to the list."""
self.modifiers.append({"name": name, "description": description})
def get_stats(self) -> dict[str, int | float | bool]:
"""Get current statistics."""
modifier_rate = (
0
if self.rounds_played == 0
else (self.modifiers_applied / self.rounds_played) * 100
)
rounds_remaining = max(0, self.total_game_rounds - self.rounds_played)
return {
"rounds_played": self.rounds_played,
"modifiers_applied": self.modifiers_applied,
"modifier_rate": round(modifier_rate, 1),
"total_game_rounds": self.total_game_rounds,
"rounds_remaining": rounds_remaining,
"is_endgame": self.is_endgame(),
"debug_mode": self.debug_mode,
"force_endgame": self.force_endgame,
}
def run(self) -> None:
"""Start the application."""
_logger.info("Texas Hold'em Modifier App started!")
_logger.info(
"Available methods: app.get_stats(), app.add_modifier(name, description)"
)
_logger.info(
"Debug features: Toggle debug mode to access force endgame controls"
)
_logger.info("Default game length: %s rounds", self.total_game_rounds)
endgame_pct = int(self.endgame_threshold * 100)
endgame_rounds = int(self.total_game_rounds * self.endgame_threshold)
_logger.info("Endgame threshold: %s%% (%s rounds)", endgame_pct, endgame_rounds)
self.root.mainloop()
if __name__ == "__main__":
app = PokerModifierApp()
app.run()

View File

@ -0,0 +1,175 @@
class PokerModifierApp {
constructor() {
this.modifiers = [
{
name: "High Stakes",
description: "All bets are doubled this round!"
},
{
name: "Wild Card",
description: "The next card revealed can be used as any card!"
},
{
name: "Bluff Master",
description: "Players can see one opponent's card before betting."
},
{
name: "All-In Fever",
description: "If someone goes all-in, everyone must match or fold."
},
{
name: "Lucky Sevens",
description: "Any hand with a 7 beats a pair!"
},
{
name: "Reverse Psychology",
description: "Lowest hand wins this round!"
},
{
name: "Split Pot",
description: "The pot is split between the top 2 hands."
},
{
name: "Texas Twister",
description: "Each player gets an extra hole card this round."
},
{
name: "Blind Luck",
description: "All players must play blind (no looking at cards) until the river."
},
{
name: "Community Boost",
description: "An extra community card is revealed (6 total)."
},
{
name: "Minimum Madness",
description: "Minimum bet is tripled this round."
},
{
name: "Suit Supremacy",
description: "All cards of the chosen suit (random) are worth +1 rank."
},
{
name: "Quick Draw",
description: "Betting time is cut in half - make decisions fast!"
},
{
name: "Royal Treatment",
description: "Face cards (J, Q, K) are worth double."
},
{
name: "Chip Challenge",
description: "Winner gets extra chips from the house!"
}
];
this.roundsPlayed = 0;
this.modifiersApplied = 0;
this.initializeElements();
this.attachEventListeners();
this.updateChanceDisplay();
}
initializeElements() {
this.startButton = document.getElementById('startRoundBtn');
this.resultDisplay = document.getElementById('resultDisplay');
this.modifierChanceSlider = document.getElementById('modifierChance');
this.chanceValueDisplay = document.getElementById('chanceValue');
this.roundsCountDisplay = document.getElementById('roundsCount');
this.modifiersCountDisplay = document.getElementById('modifiersCount');
}
attachEventListeners() {
this.startButton.addEventListener('click', () => this.startRound());
this.modifierChanceSlider.addEventListener('input', () => this.updateChanceDisplay());
}
updateChanceDisplay() {
const chance = this.modifierChanceSlider.value;
this.chanceValueDisplay.textContent = `${chance}%`;
}
startRound() {
// Add button animation
this.startButton.style.transform = 'scale(0.95)';
setTimeout(() => {
this.startButton.style.transform = '';
}, 150);
// Update round counter
this.roundsPlayed++;
this.roundsCountDisplay.textContent = this.roundsPlayed;
// Get current probability
const modifierChance = parseInt(this.modifierChanceSlider.value);
// Determine if a modifier should be applied
const randomValue = Math.random() * 100;
const shouldApplyModifier = randomValue < modifierChance;
if (shouldApplyModifier) {
this.applyRandomModifier();
} else {
this.showNoModifier();
}
// Add some visual feedback with animation
this.resultDisplay.style.opacity = '0';
this.resultDisplay.style.transform = 'scale(0.8)';
setTimeout(() => {
this.resultDisplay.style.opacity = '1';
this.resultDisplay.style.transform = 'scale(1)';
}, 200);
}
applyRandomModifier() {
// Update modifier counter
this.modifiersApplied++;
this.modifiersCountDisplay.textContent = this.modifiersApplied;
// Select random modifier
const randomIndex = Math.floor(Math.random() * this.modifiers.length);
const selectedModifier = this.modifiers[randomIndex];
// Update display
this.resultDisplay.className = 'result-display has-modifier';
this.resultDisplay.innerHTML = `
<div class="modifier-title">🎲 ${selectedModifier.name}</div>
<div class="modifier-description">${selectedModifier.description}</div>
`;
}
showNoModifier() {
this.resultDisplay.className = 'result-display no-modifier';
this.resultDisplay.innerHTML = `
<div class="no-modifier-text">No modifier this round</div>
<div style="font-size: 0.9rem; color: #999; margin-top: 0.5rem;">Play normally</div>
`;
}
// Method to add new modifiers (for future expansion)
addModifier(name, description) {
this.modifiers.push({ name, description });
}
// Method to get statistics
getStats() {
return {
roundsPlayed: this.roundsPlayed,
modifiersApplied: this.modifiersApplied,
modifierRate: this.roundsPlayed > 0 ? (this.modifiersApplied / this.roundsPlayed * 100).toFixed(1) : 0
};
}
}
// Initialize the app when the page loads
document.addEventListener('DOMContentLoaded', () => {
window.pokerApp = new PokerModifierApp();
// Add some console info for developers
console.log('🃏 Texas Hold\'em Modifier App loaded!');
console.log('Access the app instance via window.pokerApp');
console.log('Available methods: getStats(), addModifier(name, description)');
});

View File

@ -0,0 +1,234 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #0f4c3a, #1a6b4d);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.container {
max-width: 600px;
width: 90%;
padding: 2rem;
}
h1 {
text-align: center;
font-size: 2.5rem;
margin-bottom: 2rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.game-area {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 2rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.probability-settings {
margin-bottom: 2rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.probability-settings h3 {
margin-bottom: 1rem;
color: #ffd700;
text-align: center;
}
.setting {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.setting label {
font-weight: bold;
min-width: 150px;
}
#modifierChance {
flex: 1;
min-width: 150px;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
outline: none;
appearance: none;
}
#modifierChance::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
background: #ffd700;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#modifierChance::-moz-range-thumb {
width: 20px;
height: 20px;
background: #ffd700;
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#chanceValue {
font-weight: bold;
color: #ffd700;
min-width: 40px;
text-align: center;
}
.result-area {
margin: 2rem 0;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.result-display {
text-align: center;
padding: 2rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.1);
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
transition: all 0.3s ease;
}
.result-display.no-modifier {
border-color: rgba(128, 128, 128, 0.5);
background: rgba(128, 128, 128, 0.1);
}
.result-display.has-modifier {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.1);
box-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
}
.modifier-title {
font-size: 1.5rem;
font-weight: bold;
color: #ffd700;
margin-bottom: 0.5rem;
}
.modifier-description {
font-size: 1rem;
line-height: 1.4;
color: #fff;
}
.no-modifier-text {
font-size: 1.2rem;
color: #ccc;
}
.start-button {
width: 100%;
padding: 1rem 2rem;
font-size: 1.5rem;
font-weight: bold;
background: linear-gradient(45deg, #ffd700, #ffed4e);
color: #0f4c3a;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
margin-bottom: 2rem;
}
.start-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
background: linear-gradient(45deg, #ffed4e, #ffd700);
}
.start-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(255, 215, 0, 0.3);
}
.stats {
display: flex;
justify-content: space-around;
gap: 1rem;
flex-wrap: wrap;
}
.stat {
background: rgba(255, 255, 255, 0.1);
padding: 1rem;
border-radius: 8px;
text-align: center;
flex: 1;
min-width: 150px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-label {
display: block;
font-size: 0.9rem;
color: #ccc;
margin-bottom: 0.5rem;
}
.stat span:last-child {
font-size: 1.5rem;
font-weight: bold;
color: #ffd700;
}
@media (max-width: 600px) {
.container {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
.game-area {
padding: 1.5rem;
}
.setting {
flex-direction: column;
align-items: stretch;
}
.setting label {
min-width: auto;
text-align: center;
}
.stats {
flex-direction: column;
}
}

View File

@ -0,0 +1,55 @@
## Sliding-Square Puzzle Solver
Parses a screenshot of a sliding-square puzzle and solves it via BFS.
### Setup
```bash
cd puzzle_solver
python -m venv .venv && source .venv/bin/activate
pip install opencv-python-headless numpy
```
### Usage
```bash
# From workspace root, with venv active:
# Step 1 Parse screenshot to editable JSON
python -m puzzle_solver parse screenshot.png -o puzzle.json
# Step 2 Review & fix any "unknown" squares in puzzle.json
# (set "type" to: normal / portal / teleporter / key / lock)
# Step 3 Solve
python -m puzzle_solver solve puzzle.json
# One-shot (no manual review)
python -m puzzle_solver run screenshot.png
# Debug overlay (visualise detected squares on image)
python -m puzzle_solver debug screenshot.png -o debug.png
```
### Game mechanics
| Square | JSON type | Description |
| ------------------- | ------------ | ------------------------------------------------- |
| Empty outline | `normal` | Regular landing square |
| Solid fill | `player` | Starting position |
| Ring inside | `goal` | Target destination |
| Inner square offset | `portal` | Pass through from the side marked by `"side"` |
| Antenna line(s) | `teleporter` | Warp to paired teleporter (`"group"` id) |
| Key symbol | `key` | Removes matching lock (`"lock_id"`) |
| Lock symbol | `lock` | Solid until matching key collected, then vanishes |
### Movement
You slide in a cardinal direction (up/down/left/right) until you hit
another square. If you slide off the grid without hitting anything, you
die.
### Algorithm
BFS over state = `(position, set_of_active_locks)`. Explores all
reachable states and returns the shortest move sequence to the goal.

View File

@ -0,0 +1 @@
"""Sliding-square puzzle solver package."""

View File

@ -0,0 +1,5 @@
"""Allow ``python -m puzzle_solver …`` invocation."""
from python_pkg.puzzle_solver.main import main
main()

View File

@ -0,0 +1,109 @@
"""CLI for the sliding-square puzzle solver.
Usage
-----
# 1) Parse a screenshot → JSON (review & hand-edit if needed)
python puzzle_solver/main.py parse screenshot.png -o puzzle.json
# 2) Solve from JSON
python puzzle_solver/main.py solve puzzle.json
# 3) One-shot: parse + solve (skip manual review)
python puzzle_solver/main.py run screenshot.png
# 4) Draw debug overlay showing detected squares
python puzzle_solver/main.py debug screenshot.png -o debug.png
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from python_pkg.puzzle_solver.parse_image import draw_debug, parse_image, save_puzzle
from python_pkg.puzzle_solver.solver import Puzzle, print_puzzle, print_solution, solve
def cmd_parse(args: argparse.Namespace) -> None:
"""Parse a screenshot into editable puzzle JSON."""
puzzle = parse_image(args.image, threshold=args.threshold)
out = args.output or args.image.rsplit(".", 1)[0] + "_puzzle.json"
save_puzzle(puzzle, out)
if puzzle.get("notes"):
for _n in puzzle["notes"]:
pass
def cmd_solve(args: argparse.Namespace) -> None:
"""Solve a puzzle from a JSON file."""
with Path(args.puzzle).open() as f:
data = json.load(f)
puzzle = Puzzle.from_json(data)
print_puzzle(puzzle)
moves = solve(puzzle)
if moves is None:
sys.exit(1)
print_solution(puzzle, moves)
def cmd_run(args: argparse.Namespace) -> None:
"""Parse a screenshot and solve in one shot."""
data = parse_image(args.image, threshold=args.threshold)
if data.get("notes"):
for _n in data["notes"]:
pass
puzzle = Puzzle.from_json(data)
print_puzzle(puzzle)
moves = solve(puzzle)
if moves is None:
out = args.image.rsplit(".", 1)[0] + "_puzzle.json"
save_puzzle(data, out)
sys.exit(1)
print_solution(puzzle, moves)
def cmd_debug(args: argparse.Namespace) -> None:
"""Draw a debug overlay showing detected square types."""
data = parse_image(args.image, threshold=args.threshold)
out = args.output or args.image.rsplit(".", 1)[0] + "_debug.png"
draw_debug(args.image, data, out)
from collections import Counter
counts = Counter(sq["type"] for sq in data["squares"])
for _t, _n in counts.most_common():
pass
def main() -> None:
"""Entry point for the puzzle solver CLI."""
ap = argparse.ArgumentParser(description="Sliding-square puzzle solver")
sub = ap.add_subparsers(dest="command", required=True)
p_parse = sub.add_parser("parse", help="Parse screenshot → puzzle JSON")
p_parse.add_argument("image")
p_parse.add_argument("-o", "--output", help="Output JSON path")
p_parse.add_argument("-t", "--threshold", type=int, default=55)
p_solve = sub.add_parser("solve", help="Solve puzzle from JSON")
p_solve.add_argument("puzzle", help="Puzzle JSON file")
p_run = sub.add_parser("run", help="Parse + solve in one shot")
p_run.add_argument("image")
p_run.add_argument("-t", "--threshold", type=int, default=55)
p_debug = sub.add_parser("debug", help="Draw debug overlay on image")
p_debug.add_argument("image")
p_debug.add_argument("-o", "--output", help="Output image path")
p_debug.add_argument("-t", "--threshold", type=int, default=55)
args = ap.parse_args()
{"parse": cmd_parse, "solve": cmd_solve, "run": cmd_run, "debug": cmd_debug}[
args.command
](args)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,438 @@
"""Parse a puzzle screenshot into a solvable JSON representation.
Pipeline
--------
1. Threshold + contour detection find square bounding boxes
2. Cluster centres into a regular grid (row, col) for each square
3. Analyse each square's interior → classify type
4. Pair teleporters and key/lock assign group IDs
5. Export JSON (editable by hand before solving)
"""
from __future__ import annotations
import json
from pathlib import Path
import cv2
import numpy as np
_MIN_SQUARE_AREA = 80
_MAX_SQUARE_AREA = 12000
_MIN_ASPECT_RATIO = 0.45
_PLAYER_FILL_THRESHOLD = 0.40
_NORMAL_FILL_CEILING = 0.12
_MIN_INTERIOR_SIZE = 6
_RING_CIRCULARITY = 0.65
_RING_AREA_RATIO = 0.08
# ── Public API ───────────────────────────────────────────────────────
def parse_image(image_path: str, *, threshold: int = 55) -> dict:
"""Parse a screenshot and return a puzzle dict (ready for solver or JSON)."""
img = cv2.imread(image_path)
if img is None:
msg = f"Cannot load image: {image_path}"
raise FileNotFoundError(msg)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
raw = _detect_square_candidates(gray, threshold)
squares = _merge_overlapping(raw)
grid_map = _snap_to_grid(squares)
classified = _classify_all(gray, grid_map)
_assign_teleporter_and_kl_groups(classified)
return _build_output(classified)
def save_puzzle(puzzle: dict, path: str) -> None:
"""Write puzzle dict to a JSON file."""
with Path(path).open("w") as f:
json.dump(puzzle, f, indent=2)
# ── Square detection ─────────────────────────────────────────────────
def _detect_square_candidates(
gray: np.ndarray, thresh: int
) -> list[tuple[int, int, int, int]]:
_, binary = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY)
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
candidates: list[tuple[int, int, int, int]] = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
area = w * h
if area < _MIN_SQUARE_AREA or area > _MAX_SQUARE_AREA:
continue
aspect = min(w, h) / max(w, h)
if aspect < _MIN_ASPECT_RATIO:
continue
candidates.append((x, y, w, h))
return candidates
def _merge_overlapping(
candidates: list[tuple[int, int, int, int]],
) -> list[tuple[int, int, int, int]]:
"""Merge bounding boxes whose centres are very close."""
if not candidates:
return []
cands = sorted(candidates, key=lambda c: c[2] * c[3], reverse=True)
used = [False] * len(cands)
merged: list[tuple[int, int, int, int]] = []
for i, (x1, y1, w1, h1) in enumerate(cands):
if used[i]:
continue
cx1, cy1 = x1 + w1 // 2, y1 + h1 // 2
group = [(x1, y1, w1, h1)]
used[i] = True
for j in range(i + 1, len(cands)):
if used[j]:
continue
x2, y2, w2, h2 = cands[j]
cx2, cy2 = x2 + w2 // 2, y2 + h2 // 2
if (
abs(cx1 - cx2) < max(w1, w2) * 0.55
and abs(cy1 - cy2) < max(h1, h2) * 0.55
):
group.append(cands[j])
used[j] = True
gx = min(g[0] for g in group)
gy = min(g[1] for g in group)
gx2 = max(g[0] + g[2] for g in group)
gy2 = max(g[1] + g[3] for g in group)
merged.append((gx, gy, gx2 - gx, gy2 - gy))
return merged
# ── Grid snapping ────────────────────────────────────────────────────
def _cluster_values(vals: list[int], min_gap: int) -> list[int]:
if not vals:
return []
vals = sorted(vals)
clusters: list[list[int]] = [[vals[0]]]
for v in vals[1:]:
if v - clusters[-1][-1] < min_gap:
clusters[-1].append(v)
else:
clusters.append([v])
return [int(np.mean(c)) for c in clusters]
def _snap_to_grid(
squares: list[tuple[int, int, int, int]],
) -> dict[tuple[int, int], tuple[int, int, int, int]]:
centres = [(x + w // 2, y + h // 2) for x, y, w, h in squares]
xs = [c[0] for c in centres]
ys = [c[1] for c in centres]
def _median_gap(vals: list[int]) -> int:
s = sorted(set(vals))
gaps = [s[i + 1] - s[i] for i in range(len(s) - 1)]
return int(np.median(gaps)) if gaps else 30
x_gap = max(8, int(_median_gap(xs) * 0.4))
y_gap = max(8, int(_median_gap(ys) * 0.4))
x_clusters = _cluster_values(xs, x_gap)
y_clusters = _cluster_values(ys, y_gap)
grid: dict[tuple[int, int], tuple[int, int, int, int]] = {}
for sq, (cx, cy) in zip(squares, centres, strict=False):
col = min(range(len(x_clusters)), key=lambda i: abs(x_clusters[i] - cx))
row = min(range(len(y_clusters)), key=lambda i: abs(y_clusters[i] - cy))
grid[(row, col)] = sq
return grid
# ── Classification ───────────────────────────────────────────────────
def _classify_all(
gray: np.ndarray,
grid_map: dict[tuple[int, int], tuple[int, int, int, int]],
) -> dict[tuple[int, int], dict]:
classified: dict[tuple[int, int], dict] = {}
for (row, col), (x, y, w, h) in grid_map.items():
sq_type, extra = _classify_one(gray, (x, y, w, h))
classified[(row, col)] = {
"pos": [row, col],
"type": sq_type,
"pixel_center": [x + w // 2, y + h // 2],
"pixel_bbox": [x, y, w, h],
**extra,
}
return classified
Bbox = tuple[int, int, int, int]
def _classify_by_fill(
fill: float,
gray: np.ndarray,
bbox: Bbox,
interior: np.ndarray,
) -> tuple[str, dict] | None:
"""Try to classify based on fill ratio and feature detectors."""
if fill > _PLAYER_FILL_THRESHOLD:
return "player", {}
if fill < _NORMAL_FILL_CEILING:
return "normal", {}
antenna = _detect_antenna(gray, bbox)
if antenna:
return "teleporter", {"antenna_sides": antenna}
if _is_ring_pattern(interior):
return "goal", {}
return _classify_interior_feature(fill, interior)
def _classify_interior_feature(
fill: float,
interior: np.ndarray,
) -> tuple[str, dict] | None:
"""Classify portal, key/lock, or return None for unknown."""
side = _detect_portal_side(interior)
if side:
return "portal", {"side": side}
if _has_interior_feature(interior):
return "key_or_lock", {"fill_ratio": round(fill, 3)}
return None
def _classify_one(
gray: np.ndarray,
bbox: Bbox,
) -> tuple[str, dict]:
x, y, w, h = bbox
border = max(3, min(w, h) // 5)
ix1, iy1 = x + border, y + border
ix2, iy2 = x + w - border, y + h - border
if ix2 <= ix1 or iy2 <= iy1:
return "normal", {}
interior = gray[iy1:iy2, ix1:ix2]
fill = float(np.mean(interior) / 255.0)
result = _classify_by_fill(fill, gray, bbox, interior)
if result is not None:
return result
return "unknown", {"fill_ratio": round(fill, 3)}
# ── Feature detectors ────────────────────────────────────────────────
def _detect_antenna(
gray: np.ndarray,
bbox: Bbox,
margin: int = 8,
thr: float = 0.08,
) -> list[str] | None:
"""Check for bright pixels in a narrow strip outside each edge."""
x, y, w, h = bbox
ih, iw = gray.shape
sides: list[str] = []
qw, qh = w // 4, h // 4 # quarter-width / height
# up
if y > margin:
s = gray[y - margin : y - 1, x + qw : x + w - qw]
if s.size and float(np.mean(s) / 255) > thr:
sides.append("up")
# down
if y + h + margin < ih:
s = gray[y + h + 1 : y + h + margin, x + qw : x + w - qw]
if s.size and float(np.mean(s) / 255) > thr:
sides.append("down")
# left
if x > margin:
s = gray[y + qh : y + h - qh, x - margin : x - 1]
if s.size and float(np.mean(s) / 255) > thr:
sides.append("left")
# right
if x + w + margin < iw:
s = gray[y + qh : y + h - qh, x + w + 1 : x + w + margin]
if s.size and float(np.mean(s) / 255) > thr:
sides.append("right")
return sides or None
def _is_ring_pattern(interior: np.ndarray) -> bool:
h, w = interior.shape
if h < _MIN_INTERIOR_SIZE or w < _MIN_INTERIOR_SIZE:
return False
_, bw = cv2.threshold(interior, 40, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(bw, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
area = cv2.contourArea(cnt)
peri = cv2.arcLength(cnt, closed=True)
if peri == 0:
continue
circ = 4 * np.pi * area / (peri * peri)
if circ > _RING_CIRCULARITY and area > (h * w) * _RING_AREA_RATIO:
return True
return False
def _detect_portal_side(interior: np.ndarray) -> str | None:
h, w = interior.shape
if h < _MIN_INTERIOR_SIZE or w < _MIN_INTERIOR_SIZE:
return None
thirds_w, thirds_h = w // 3, h // 3
regions = {
"left": float(np.mean(interior[:, :thirds_w])),
"right": float(np.mean(interior[:, w - thirds_w :])),
"up": float(np.mean(interior[:thirds_h, :])),
"down": float(np.mean(interior[h - thirds_h :, :])),
}
best = max(regions, key=lambda k: regions[k])
opposites = {"left": "right", "right": "left", "up": "down", "down": "up"}
opp = regions[opposites[best]]
if regions[best] > max(opp * 2.5, 8):
return best
return None
def _has_interior_feature(interior: np.ndarray) -> bool:
_, bw = cv2.threshold(interior, 40, 255, cv2.THRESH_BINARY)
total_white = int(np.sum(bw > 0))
return total_white > interior.size * 0.06
# ── Teleporter / key-lock grouping ───────────────────────────────────
def _assign_teleporter_and_kl_groups(classified: dict[tuple[int, int], dict]) -> None:
# ── Teleporters ──
tele = [(p, d) for p, d in classified.items() if d["type"] == "teleporter"]
gid = 1
used: set[tuple[int, int]] = set()
for i, (p1, d1) in enumerate(tele):
if p1 in used:
continue
s1 = set(d1.get("antenna_sides", []))
for p2, d2 in tele[i + 1 :]:
if p2 in used:
continue
s2 = set(d2.get("antenna_sides", []))
if s1 == s2:
d1["group"] = gid
d2["group"] = gid
used |= {p1, p2}
gid += 1
break
# pair any remaining teleporters sequentially
unpaired = [
p
for p, d in classified.items()
if d["type"] == "teleporter" and "group" not in d
]
for i in range(0, len(unpaired) - 1, 2):
classified[unpaired[i]]["group"] = gid
classified[unpaired[i + 1]]["group"] = gid
gid += 1
# ── Key / lock ──
kl = [p for p, d in classified.items() if d["type"] == "key_or_lock"]
lid = 1
for i in range(0, len(kl) - 1, 2):
classified[kl[i]]["type"] = "key"
classified[kl[i]]["lock_id"] = lid
classified[kl[i + 1]]["type"] = "lock"
classified[kl[i + 1]]["lock_id"] = lid
lid += 1
# odd one out → mark unknown
if len(kl) % 2:
classified[kl[-1]]["type"] = "unknown"
# ── Output builder ───────────────────────────────────────────────────
def _build_output(classified: dict[tuple[int, int], dict]) -> dict:
squares: list[dict] = []
notes: list[str] = []
for pos in sorted(classified):
d = classified[pos]
sq: dict = {"pos": d["pos"], "type": d["type"]}
if "side" in d:
sq["side"] = d["side"]
if "group" in d:
sq["group"] = d["group"]
if "lock_id" in d:
sq["lock_id"] = d["lock_id"]
# keep pixel info for debugging (prefixed with _)
sq["_pixel_center"] = d["pixel_center"]
sq["_pixel_bbox"] = d["pixel_bbox"]
if d["type"] == "unknown":
notes.append(
f"grid {d['pos']} pixel {d['pixel_center']}: "
f"classified 'unknown' (fill={d.get('fill_ratio', '?')}) "
"- edit type manually"
)
squares.append(sq)
return {"squares": squares, "notes": notes}
# ── Debug visualisation ──────────────────────────────────────────────
TYPE_COLOURS = {
"normal": (200, 200, 200),
"player": (0, 255, 0),
"goal": (0, 200, 255),
"portal": (255, 100, 0),
"teleporter": (255, 0, 255),
"key": (0, 255, 255),
"lock": (0, 0, 255),
"key_or_lock": (100, 100, 255),
"unknown": (0, 0, 200),
}
def draw_debug(image_path: str, puzzle: dict, output_path: str) -> None:
"""Draw classified squares on the image and save for visual verification."""
img = cv2.imread(image_path)
if img is None:
return
for sq in puzzle["squares"]:
x, y, w, h = sq["_pixel_bbox"]
colour = TYPE_COLOURS.get(sq["type"], (128, 128, 128))
cv2.rectangle(img, (x, y), (x + w, y + h), colour, 2)
label = sq["type"][0].upper()
if sq["type"] == "portal":
arrows = {"left": "<", "right": ">", "up": "^", "down": "v"}
label = arrows.get(sq.get("side", ""), "O")
cv2.putText(
img, label, (x + 2, y + h - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.4, colour, 1
)
cv2.imwrite(output_path, img)

View File

@ -0,0 +1,330 @@
"""BFS puzzle solver for sliding-square puzzles.
The player slides in one of 4 directions until hitting a square (or dies
if no square is reached). Special square types modify traversal:
- PORTAL: pass-through when approached from the marked side
- TELEPORTER: warp to paired teleporter on landing
- KEY: removes the matching LOCK square from the board
- LOCK: solid until its KEY is collected, then disappears
"""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass
from enum import Enum
import json
from pathlib import Path
from typing import Any
# ── Direction helpers ────────────────────────────────────────────────
UP = (-1, 0)
DOWN = (1, 0)
LEFT = (0, -1)
RIGHT = (0, 1)
DIRECTIONS: dict[str, tuple[int, int]] = {
"up": UP,
"down": DOWN,
"left": LEFT,
"right": RIGHT,
}
# When moving in a direction, which side of the target square do we approach?
DIR_TO_APPROACH_SIDE: dict[tuple[int, int], str] = {
RIGHT: "left",
LEFT: "right",
DOWN: "up",
UP: "down",
}
# ── Data model ───────────────────────────────────────────────────────
class SquareType(Enum):
"""Types of squares in the puzzle grid."""
NORMAL = "normal"
PLAYER = "player"
GOAL = "goal"
PORTAL = "portal"
TELEPORTER = "teleporter"
KEY = "key"
LOCK = "lock"
@dataclass(frozen=True)
class Square:
"""A single square on the puzzle board."""
pos: tuple[int, int]
square_type: SquareType
portal_side: str | None = None # PORTAL: side with inner square
teleporter_group: int | None = None # TELEPORTER: pair id
lock_id: int | None = None # KEY / LOCK: matching id
@dataclass(frozen=True)
class State:
"""Immutable snapshot of player position and remaining locks."""
pos: tuple[int, int]
active_locks: frozenset[tuple[int, int]]
@dataclass
class _ParseMetadata:
"""Intermediate bookkeeping collected while parsing squares."""
player_start: tuple[int, int]
goal_pos: tuple[int, int]
teleporter_groups: dict[int, list[tuple[int, int]]]
key_map: dict[int, tuple[int, int]]
lock_map: dict[int, tuple[int, int]]
def _parse_square_list(
square_dicts: list[dict[str, Any]],
) -> tuple[dict[tuple[int, int], Square], _ParseMetadata]:
"""Parse the JSON squares list into Square objects and metadata."""
squares: dict[tuple[int, int], Square] = {}
player_start: tuple[int, int] | None = None
goal_pos: tuple[int, int] | None = None
teleporter_groups: dict[int, list[tuple[int, int]]] = {}
key_map: dict[int, tuple[int, int]] = {}
lock_map: dict[int, tuple[int, int]] = {}
for sd in square_dicts:
pos = (int(sd["pos"][0]), int(sd["pos"][1]))
sq_type = SquareType(sd["type"])
sq = Square(
pos=pos,
square_type=sq_type,
portal_side=sd.get("side"),
teleporter_group=sd.get("group"),
lock_id=sd.get("lock_id"),
)
squares[pos] = sq
if sq_type == SquareType.PLAYER:
player_start = pos
elif sq_type == SquareType.GOAL:
goal_pos = pos
elif sq_type == SquareType.TELEPORTER and sq.teleporter_group is not None:
teleporter_groups.setdefault(sq.teleporter_group, []).append(pos)
elif sq_type == SquareType.KEY and sq.lock_id is not None:
key_map[sq.lock_id] = pos
elif sq_type == SquareType.LOCK and sq.lock_id is not None:
lock_map[sq.lock_id] = pos
if player_start is None:
msg = "No player start position found in puzzle data"
raise ValueError(msg)
if goal_pos is None:
msg = "No goal position found in puzzle data"
raise ValueError(msg)
metadata = _ParseMetadata(
player_start, goal_pos, teleporter_groups, key_map, lock_map
)
return squares, metadata
def _pair_teleporters(
groups: dict[int, list[tuple[int, int]]],
) -> dict[tuple[int, int], tuple[int, int]]:
"""Pair up teleporter squares by group id."""
pairs: dict[tuple[int, int], tuple[int, int]] = {}
expected_pair_size = 2
for gid, positions in groups.items():
if len(positions) != expected_pair_size:
msg = f"Teleporter group {gid} has {len(positions)} members (need 2)"
raise ValueError(msg)
pairs[positions[0]] = positions[1]
pairs[positions[1]] = positions[0]
return pairs
def _map_keys_to_locks(
key_map: dict[int, tuple[int, int]],
lock_map: dict[int, tuple[int, int]],
) -> dict[tuple[int, int], tuple[int, int]]:
"""Map each key position to its corresponding lock position."""
key_to_lock: dict[tuple[int, int], tuple[int, int]] = {}
for lid, kpos in key_map.items():
if lid not in lock_map:
msg = f"Key with lock_id={lid} has no matching lock"
raise ValueError(msg)
key_to_lock[kpos] = lock_map[lid]
return key_to_lock
@dataclass
class Puzzle:
"""Full puzzle definition with squares, teleporters, and key-lock pairs."""
squares: dict[tuple[int, int], Square]
player_start: tuple[int, int]
goal_pos: tuple[int, int]
teleporter_pairs: dict[tuple[int, int], tuple[int, int]]
key_to_lock: dict[tuple[int, int], tuple[int, int]]
grid_bounds: tuple[int, int, int, int] # min_r, max_r, min_c, max_c
# ── JSON round-trip ──────────────────────────────────────────────
@classmethod
def from_json(cls, data: dict[str, Any]) -> Puzzle:
"""Build a Puzzle from a parsed JSON dict."""
squares, metadata = _parse_square_list(data["squares"])
teleporter_pairs = _pair_teleporters(metadata.teleporter_groups)
key_to_lock = _map_keys_to_locks(metadata.key_map, metadata.lock_map)
all_pos = list(squares)
rows = [p[0] for p in all_pos]
cols = [p[1] for p in all_pos]
bounds = (min(rows) - 1, max(rows) + 1, min(cols) - 1, max(cols) + 1)
return cls(
squares,
metadata.player_start,
metadata.goal_pos,
teleporter_pairs,
key_to_lock,
bounds,
)
@classmethod
def from_file(cls, path: str) -> Puzzle:
"""Load a Puzzle from a JSON file path."""
with Path(path).open() as f:
return cls.from_json(json.load(f))
# ── Solver ───────────────────────────────────────────────────────────
def solve(puzzle: Puzzle) -> list[str] | None:
"""BFS over (position, active_locks) states. Returns move list or None."""
initial_locks = frozenset(
sq.pos for sq in puzzle.squares.values() if sq.square_type == SquareType.LOCK
)
start = State(puzzle.player_start, initial_locks)
queue: deque[tuple[State, list[str]]] = deque([(start, [])])
visited: set[State] = {start}
while queue:
state, path = queue.popleft()
for dir_name, (dr, dc) in DIRECTIONS.items():
result = _simulate_move(puzzle, state, dr, dc)
if result is None:
continue
new_state, reached_goal = result
if reached_goal:
return [*path, dir_name]
if new_state not in visited:
visited.add(new_state)
queue.append((new_state, [*path, dir_name]))
return None
def _simulate_move(
puzzle: Puzzle,
state: State,
dr: int,
dc: int,
) -> tuple[State, bool] | None:
"""Slide in (dr, dc). Returns (new_state, is_goal) or None on death."""
r, c = state.pos
min_r, max_r, min_c, max_c = puzzle.grid_bounds
approach_side = DIR_TO_APPROACH_SIDE[(dr, dc)]
cr, cc = r + dr, c + dc
while min_r <= cr <= max_r and min_c <= cc <= max_c:
pos = (cr, cc)
if pos in puzzle.squares:
sq = puzzle.squares[pos]
# Vanished lock - slide through
if sq.square_type == SquareType.LOCK and pos not in state.active_locks:
cr += dr
cc += dc
continue
# Portal pass-through when approached from marked side
if sq.square_type == SquareType.PORTAL and sq.portal_side == approach_side:
cr += dr
cc += dc
continue
# ── Landing ──
if sq.square_type == SquareType.GOAL:
return State(pos, state.active_locks), True
if (
sq.square_type == SquareType.TELEPORTER
and pos in puzzle.teleporter_pairs
):
return State(puzzle.teleporter_pairs[pos], state.active_locks), False
if sq.square_type == SquareType.KEY and pos in puzzle.key_to_lock:
lock_pos = puzzle.key_to_lock[pos]
return State(pos, state.active_locks - {lock_pos}), False
# Default: land on square
return State(pos, state.active_locks), False
cr += dr
cc += dc
return None # off-grid → death
# ── Pretty-print ─────────────────────────────────────────────────────
_TYPE_CHAR = {
SquareType.NORMAL: ".",
SquareType.PLAYER: "P",
SquareType.GOAL: "G",
SquareType.PORTAL: "O",
SquareType.TELEPORTER: "T",
SquareType.KEY: "K",
SquareType.LOCK: "L",
}
def print_puzzle(puzzle: Puzzle) -> None:
"""Print an ASCII representation of the puzzle grid."""
min_r, max_r, min_c, max_c = puzzle.grid_bounds
for r in range(min_r + 1, max_r):
row_chars: list[str] = []
for c in range(min_c + 1, max_c):
if (r, c) in puzzle.squares:
sq = puzzle.squares[(r, c)]
ch = _TYPE_CHAR.get(sq.square_type, "?")
if sq.square_type == SquareType.PORTAL and sq.portal_side:
arrow = {"left": "<", "right": ">", "up": "^", "down": "v"}
ch = arrow.get(sq.portal_side, "O")
row_chars.append(ch)
else:
row_chars.append(" ")
def print_solution(puzzle: Puzzle, moves: list[str]) -> None:
"""Print the solution path step by step."""
state = State(
puzzle.player_start,
frozenset(
sq.pos
for sq in puzzle.squares.values()
if sq.square_type == SquareType.LOCK
),
)
for _i, move in enumerate(moves, 1):
dr, dc = DIRECTIONS[move]
result = _simulate_move(puzzle, state, dr, dc)
if result is None:
return
state, _goal = result