mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 15:43:11 +02:00
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:
parent
78c1d77144
commit
4cf523bf6d
16
python_pkg/articles/.clang-format
Normal file
16
python_pkg/articles/.clang-format
Normal 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
2
python_pkg/articles/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
data/*
|
||||||
|
uploads/*
|
||||||
61
python_pkg/articles/Makefile
Normal file
61
python_pkg/articles/Makefile
Normal 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
|
||||||
17
python_pkg/articles/README.md
Normal file
17
python_pkg/articles/README.md
Normal 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`.
|
||||||
1
python_pkg/articles/__init__.py
Normal file
1
python_pkg/articles/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Articles site tests package."""
|
||||||
478
python_pkg/articles/cppcheck.txt
Normal file
478
python_pkg/articles/cppcheck.txt
Normal 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]
|
||||||
24
python_pkg/articles/data/articles.json
Normal file
24
python_pkg/articles/data/articles.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
233
python_pkg/articles/index.html
Normal file
233
python_pkg/articles/index.html
Normal 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=>({"&":"&","<":"<",">":">"}[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
33
python_pkg/articles/run.sh
Executable 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"
|
||||||
19
python_pkg/articles/run_tests.sh
Executable file
19
python_pkg/articles/run_tests.sh
Executable 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
|
||||||
1640
python_pkg/articles/server_c.c
Normal file
1640
python_pkg/articles/server_c.c
Normal file
File diff suppressed because it is too large
Load Diff
31
python_pkg/articles/sw.js
Normal file
31
python_pkg/articles/sw.js
Normal 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;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
});
|
||||||
109
python_pkg/articles/test_server_api.py
Normal file
109
python_pkg/articles/test_server_api.py
Normal 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()
|
||||||
20
python_pkg/articles/test_site_size.py
Normal file
20
python_pkg/articles/test_site_size.py
Normal 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}"
|
||||||
31
python_pkg/articles/tools/funcsize.awk
Normal file
31
python_pkg/articles/tools/funcsize.awk
Normal 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 }
|
||||||
1
python_pkg/moviepy_showcase/__init__.py
Normal file
1
python_pkg/moviepy_showcase/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""MoviePy 2.x comprehensive showcase package."""
|
||||||
357
python_pkg/moviepy_showcase/_moviepy_audio_output.py
Normal file
357
python_pkg/moviepy_showcase/_moviepy_audio_output.py
Normal 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
|
||||||
282
python_pkg/moviepy_showcase/_moviepy_clip_types.py
Normal file
282
python_pkg/moviepy_showcase/_moviepy_clip_types.py
Normal 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
|
||||||
336
python_pkg/moviepy_showcase/_moviepy_video_effects.py
Normal file
336
python_pkg/moviepy_showcase/_moviepy_video_effects.py
Normal 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
|
||||||
306
python_pkg/moviepy_showcase/moviepy_showcase.py
Normal file
306
python_pkg/moviepy_showcase/moviepy_showcase.py
Normal 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()
|
||||||
58
python_pkg/poker_modifier_app/README.md
Normal file
58
python_pkg/poker_modifier_app/README.md
Normal 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).
|
||||||
138
python_pkg/poker_modifier_app/README_python.md
Normal file
138
python_pkg/poker_modifier_app/README_python.md
Normal 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
|
||||||
1
python_pkg/poker_modifier_app/__init__.py
Normal file
1
python_pkg/poker_modifier_app/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Poker modifier application package."""
|
||||||
303
python_pkg/poker_modifier_app/_poker_gui.py
Normal file
303
python_pkg/poker_modifier_app/_poker_gui.py
Normal 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)
|
||||||
465
python_pkg/poker_modifier_app/_poker_modifiers.py
Normal file
465
python_pkg/poker_modifier_app/_poker_modifiers.py
Normal 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."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
45
python_pkg/poker_modifier_app/index.html
Normal file
45
python_pkg/poker_modifier_app/index.html
Normal 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>
|
||||||
266
python_pkg/poker_modifier_app/poker_modifier_app.py
Normal file
266
python_pkg/poker_modifier_app/poker_modifier_app.py
Normal 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()
|
||||||
175
python_pkg/poker_modifier_app/script.js
Normal file
175
python_pkg/poker_modifier_app/script.js
Normal 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)');
|
||||||
|
});
|
||||||
234
python_pkg/poker_modifier_app/style.css
Normal file
234
python_pkg/poker_modifier_app/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
python_pkg/puzzle_solver/README.md
Normal file
55
python_pkg/puzzle_solver/README.md
Normal 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.
|
||||||
1
python_pkg/puzzle_solver/__init__.py
Normal file
1
python_pkg/puzzle_solver/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Sliding-square puzzle solver package."""
|
||||||
5
python_pkg/puzzle_solver/__main__.py
Normal file
5
python_pkg/puzzle_solver/__main__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Allow ``python -m puzzle_solver …`` invocation."""
|
||||||
|
|
||||||
|
from python_pkg.puzzle_solver.main import main
|
||||||
|
|
||||||
|
main()
|
||||||
109
python_pkg/puzzle_solver/main.py
Normal file
109
python_pkg/puzzle_solver/main.py
Normal 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()
|
||||||
438
python_pkg/puzzle_solver/parse_image.py
Normal file
438
python_pkg/puzzle_solver/parse_image.py
Normal 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)
|
||||||
330
python_pkg/puzzle_solver/solver.py
Normal file
330
python_pkg/puzzle_solver/solver.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user