mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 11:43:13 +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