Files
deepseek f040bb6be0 Three Kingdoms: Warlord's Fate — complete playable game
- Stylized 3D ink-painting map of China (12 provinces, 32 cities, 17 factions)
- Custom warlord creation (8 origins, banner, starting city) or historical factions
- City management: 7 buildings, 5 dev tiers, recruitment from levies
- Character system: stats, traits, loyalty, relationships, wounds, capture, death, succession
- Turn-based tactical battles with formations, stances, hero skills, cinematic 3D replay
- Sieges: assault, starvation, bribery, infiltration
- Diplomacy with trust memory, alliances, NAPs, trade, marriage, espionage, betrayal
- Scripted diverging history (Dong Zhuo, Guandu, Red Cliffs...) + world crises + court events
- AI factions with distinct personalities; prisoners (execute/release/recruit/ransom)
- Procedural guqin/taiko WebAudio score; save/load; victory + dynasty chronicle screens
- View-relative camera controls; headless test suites (smoke, stress, map validator)
2026-08-23 06:59:40 +00:00

161 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Programmatic visual review of screenshots: OCR geometry, contrast, palette, composition."""
import sys, subprocess, csv, io, math
from PIL import Image
import numpy as np
SHOTS = sys.argv[1:]
LANG = "eng+chi_sim"
def rel_lum(arr):
a = arr.astype(np.float64) / 255.0
def f(c): return np.where(c <= 0.03928, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
return 0.2126 * f(a[..., 0]) + 0.7152 * f(a[..., 1]) + 0.0722 * f(a[..., 2])
def cr(l1, l2):
hi, lo = max(l1, l2), min(l1, l2)
return (hi + 0.05) / (lo + 0.05)
def hexc(rgb):
return "#%02x%02x%02x" % tuple(int(x) for x in rgb)
def ocr_lines(img_path):
"""Return list of (text, x,y,w,h, conf) grouped per line."""
p = subprocess.run(
["tesseract", img_path, "stdout", "-l", LANG, "--psm", "11", "tsv"],
capture_output=True, text=True)
rows = list(csv.DictReader(io.StringIO(p.stdout), delimiter="\t"))
lines = {}
for r in rows:
try:
conf = float(r["conf"])
except Exception:
continue
if conf < 35 or not r["text"].strip():
continue
key = (r["block_num"], r["par_num"], r["line_num"])
e = lines.setdefault(key, {"words": [], "x0": 10**9, "y0": 10**9, "x1": -1, "y1": -1, "confs": []})
x, y, w, h = int(r["left"]), int(r["top"]), int(r["width"]), int(r["height"])
e["words"].append((r["text"], conf))
e["confs"].append(conf)
e["x0"] = min(e["x0"], x); e["y0"] = min(e["y0"], y)
e["x1"] = max(e["x1"], x + w); e["y1"] = max(e["y1"], y + h)
out = []
for k in sorted(lines, key=lambda k: (lines[k]["y0"], lines[k]["x0"])):
e = lines[k]
out.append({"text": " ".join(w for w, _ in e["words"]),
"bbox": (e["x0"], e["y0"], e["x1"] - e["x0"], e["y1"] - e["y0"]),
"conf": sum(e["confs"]) / len(e["confs"])})
return out
def analyze(path):
im = Image.open(path).convert("RGB")
W, H = im.size
arr = np.asarray(im)
L = rel_lum(arr)
gray = np.asarray(im.convert("L"))
print(f"\n{'='*78}\n{path} ({W}x{H})\n{'='*78}")
# Global tone
print(f"luminance mean={L.mean():.3f} p5={np.percentile(L,5):.3f} p50={np.percentile(L,50):.3f} "
f"p95={np.percentile(L,95):.3f} | dark(<0.15)={100*(L<0.15).mean():.0f}% bright(>0.85)={100*(L>0.85).mean():.0f}%")
# Palette
q = im.quantize(colors=8, method=Image.Quantize.MEDIANCUT)
pal = q.getpalette()
counts = sorted(q.getcolors(W * H), reverse=True)
tot = W * H
tops = []
for cnt, idx in counts[:8]:
rgb = pal[idx*3:idx*3+3]
tops.append(f"{hexc(rgb)} {100*cnt/tot:.0f}%")
print("palette: " + " ".join(tops))
# Composition grid: 16 cols x 10 rows -> mean luminance (0-9) and dominant hue class letter
GX, GY = 16, 10
print("composition grid (mean lum 0-9; hue: R=red/warm Y=yellow/tan G=green C=cyan B=blue M=magenta K=neutral-dark N=neutral-mid W=white-ish):")
hsv = np.asarray(im.convert("HSV")).astype(int)
hh, ss, vv = hsv[..., 0], hsv[..., 1], hsv[..., 2]
for gy in range(GY):
row = ""
for gx in range(GX):
cell = L[gy*H//GY:(gy+1)*H//GY, gx*W//GX:(gx+1)*W//GX]
m = cell.mean()
# dominant hue of saturated pixels
ch = hh[gy*H//GY:(gy+1)*H//GY, gx*W//GX:(gx+1)*W//GX]
cs = ss[gy*H//GY:(gy+1)*H//GY, gx*W//GX:(gx+1)*W//GX]
sat = cs > 60
if m > 0.85: c = "W"
elif sat.mean() > 0.12:
ang = np.median(ch[sat]) * 360 / 255
c = "R" if ang < 25 or ang >= 330 else "Y" if ang < 70 else "G" if ang < 160 else "C" if ang < 200 else "B" if ang < 270 else "M"
elif m > 0.45: c = "N"
else: c = "K"
row += str(min(9, int(m * 10))) + c
print(" " + row)
# Low-variance (flat) regions — candidate dead space
flat = []
for gy in range(GY):
for gx in range(GX):
cell = gray[gy*H//GY:(gy+1)*H//GY, gx*W//GX:(gx+1)*W//GX]
if cell.std() < 6:
flat.append((gx, gy, round(float(cell.mean()))))
print(f"flat cells (std<6): {len(flat)}/{GX*GY}" + (f" at {flat[:12]}" if flat else ""))
# OCR line analysis
lines = ocr_lines(path)
print(f"OCR: {len(lines)} lines (conf>=35)")
low_contrast, overlaps, tiny = [], [], []
boxes = []
for ln in lines:
x, y, w, h = ln["bbox"]
if w <= 1 or h <= 1 or x < 0 or y < 0 or x + w > W or y + h > H:
continue
pad = 2
x0, y0 = max(0, x - pad), max(0, y - pad)
x1, y1 = min(W, x + w + pad), min(H, y + h + pad)
reg = L[y0:y1, x0:x1]
lo, hi = np.percentile(reg, 5), np.percentile(reg, 95)
ratio = cr(hi, lo)
boxes.append((x, y, w, h, ln["text"]))
if h < 9 and ln["conf"] > 50:
tiny.append(ln)
if ratio < 4.0 and len(ln["text"].strip()) > 1:
low_contrast.append((ln["text"][:40], (x, y, w, h), round(ratio, 2)))
# overlap between different lines (>25% of smaller area)
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
ax, ay, aw, ah, at = boxes[i]; bx, by, bw, bh, bt = boxes[j]
ox = max(0, min(ax+aw, bx+bw) - max(ax, bx)); oy = max(0, min(ay+ah, by+bh) - max(ay, by))
inter = ox * oy
small = min(aw*ah, bw*bh)
if small and inter / small > 0.30 and at != bt:
overlaps.append((at[:28], bt[:28], round(100*inter/small)))
if low_contrast:
print("LOW CONTRAST (<4:1):")
for t, b, r in low_contrast[:14]:
print(f" {r:>5}:1 @({b[0]},{b[1]},{b[2]}x{b[3]}) '{t}'")
if len(low_contrast) > 14: print(f" ... +{len(low_contrast)-14} more")
else:
print("LOW CONTRAST: none flagged")
if overlaps:
print("OVERLAPPING TEXT BOXES:")
for a, b, pct in overlaps[:10]:
print(f" {pct}% '{a}' vs '{b}'")
if tiny:
print(f"VERY SMALL TEXT (<9px tall): {len(tiny)} e.g. " + "; ".join(t['text'][:18] for t in tiny[:5]))
# Print all OCR lines with geometry so reviewer can reconstruct layout
print("--- OCR transcript (x,y w h | conf | text) ---")
for ln in lines:
x, y, w, h = ln["bbox"]
print(f" ({x:>4},{y:>4} {w:>4}x{h:<3}|{ln['conf']:.0f}) {ln['text'][:90]}")
for p in SHOTS:
try:
analyze(p)
except Exception as e:
print(f"\nFAILED {p}: {type(e).__name__}: {e}")