Skip to content

Commit 7a5acd5

Browse files
committed
feat: Randonautica-inspired exploration tool, honest framing (v1.1.9)
- scripts/explore_cast.py: QRNG (reuses entropy.py) -> uniform random points in radius -> dependency-free grid-density anomaly (attractor/void/power/blindspot, clamped within the circle) -> bearing/distance/compass + 黄历 吉方 alignment + safety block. Explicit disclaimer: randomized walk prompt, NOT prediction, NOT MMI; intention is recorded, never biases entropy. - tests/test_explore.py (+12, 989->1001): within-radius all modes, determinism, geometry, validation, 黄历 alignment, safety/disclaimer. - SKILL.md routes 随机寻访/探索 to it. - Borrowed only legit tech (QRNG + density anomaly + intention UX + safety); rejected the MMI "intention biases RNG" pseudoscience.
1 parent ed41dd8 commit 7a5acd5

6 files changed

Lines changed: 337 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## [1.1.9] — 2026-05-31
6+
7+
Randonautica-inspired exploration tool (honest, no pseudoscience).
8+
9+
### Added
10+
- `scripts/explore_cast.py` — 今日随机寻访点: QRNG (reuses `entropy.py`) →
11+
uniform random points in a radius → dependency-free grid-density anomaly
12+
(attractor / void / power / blindspot, all clamped inside the circular
13+
radius) → bearing + distance + 16-point compass, cross-referenced with
14+
today's 黄历 吉神方位 (财神/喜神/福神). Carries a safety block and an explicit
15+
disclaimer: it is a randomized walk prompt, NOT a prediction and NOT a
16+
mind-matter-interaction (MMI) device — intention is recorded, never biases
17+
the entropy. SKILL.md routes 随机寻访/探索 to it.
18+
- `tests/test_explore.py` (+12, suite 989 → 1001) — within-radius for all 4
19+
modes, seed determinism, geometry (haversine/bearing/compass), input
20+
validation, 黄历 alignment, safety/disclaimer presence.
21+
22+
### Note
23+
Borrowed only the *legitimate* tech from Randonautica (QRNG + spatial density
24+
anomaly + intention UX + safety). The "intention biases quantum RNG / z-score =
25+
psi" MMI claim is explicitly rejected, consistent with this project's stance
26+
that divination efficacy is not a physically-measurable quantity.
27+
528
## [1.1.8] — 2026-05-31
629

730
Optional quantum entropy source for divination casts.

SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ When a reading needs personal data (八字 / 紫微 / 合婚 / 起名 / 择日),
5959
| 神煞 / 桃花 / 驿马 / 天乙贵人 | 神煞详表 | [19-shensha.md](references/19-shensha.md) ||
6060
| 小六壬 / 大安留连速喜 | 小六壬快占 | [21-extended-methods.md](references/21-extended-methods.md) | [xiaoliuren_cast.py](scripts/xiaoliuren_cast.py) |
6161
| 太乙 / 铁板 / 称骨 / 河洛 / 七政四余 / 灵签 / 杯筊 / 玄空飞星 | 扩展术数索引 | [21-extended-methods.md](references/21-extended-methods.md) ||
62+
| 随机寻访 / 今日探索点 / 出门走走去哪 / QRNG 探索 | 随机探索 (非占卜) || [explore_cast.py](scripts/explore_cast.py) |
6263
| 五行 / 天干地支 / 阴阳 / 八卦 (理论) | 基础理论 | [00-foundations.md](references/00-foundations.md) ||
6364

6465
When the user request is ambiguous, ask **one** clarifying question (preferred method? specific concern: 财运/感情/事业/健康?) and proceed.

scripts/bazi_calc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
warn,
4949
)
5050

51-
VERSION = "1.1.8"
51+
VERSION = "1.1.9"
5252
ASSETS_DIR = Path(__file__).resolve().parents[1] / "assets"
5353

5454

scripts/explore_cast.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""今日随机寻访点 — a QRNG-driven exploration prompt (Randonautica-inspired).
2+
3+
Generates a random nearby point from quantum/CSPRNG entropy, finds a
4+
statistically anomalous spot (attractor = dense cluster, void = sparse,
5+
power = strongest, blindspot = plain random), reports bearing + distance, and
6+
notes how it lines up with today's almanac auspicious directions (黄历吉方).
7+
8+
HONEST FRAMING — read before use:
9+
This is a randomized *walk / exploration prompt*, NOT a prediction and NOT a
10+
"mind-matter interaction" (MMI) device. Attractor/void points are pure
11+
statistical density fluctuations in random data; setting an intention does NOT
12+
physically bias the entropy, and no point is "influenced by thought". The 黄历
13+
alignment is a cultural overlay for fun, not a claim of efficacy. Output always
14+
carries this disclaimer plus safety guidance — going to unfamiliar places has
15+
real-world risk.
16+
17+
Usage:
18+
python explore_cast.py --lat -33.8688 --lon 151.2093 --radius 3000 \\
19+
--mode attractor --entropy quantum --intention "灵感"
20+
"""
21+
from __future__ import annotations
22+
23+
import argparse
24+
import math
25+
import sys
26+
27+
import entropy
28+
from utils import json_print
29+
30+
EARTH_M_PER_DEG = 111_320.0 # metres per degree of latitude (mean)
31+
32+
# 后天八卦 -> compass bearing (degrees, 0=N clockwise).
33+
BAGUA_BEARING: dict[str, float] = {
34+
"坎": 0.0, "艮": 45.0, "震": 90.0, "巽": 135.0,
35+
"离": 180.0, "坤": 225.0, "兑": 270.0, "乾": 315.0,
36+
}
37+
38+
SAFETY = [
39+
"白天前往, 勿夜间独行",
40+
"结伴而行, 告知他人去向",
41+
"守法; 勿进入私人领地 / 危险区 / 工地 / 水域",
42+
"随机点仅为探索提示, 后果自负, 不适宜则折返",
43+
]
44+
45+
46+
def random_points(lat: float, lon: float, radius_m: float, n: int, rng):
47+
"""n uniformly-distributed points within radius_m of (lat, lon)."""
48+
pts = []
49+
cos_lat = math.cos(math.radians(lat)) or 1e-9
50+
for _ in range(n):
51+
r = radius_m * math.sqrt(rng.random()) # sqrt for uniform area
52+
theta = 2 * math.pi * rng.random()
53+
dx = r * math.cos(theta)
54+
dy = r * math.sin(theta)
55+
pts.append((lat + dy / EARTH_M_PER_DEG,
56+
lon + dx / (EARTH_M_PER_DEG * cos_lat)))
57+
return pts
58+
59+
60+
def find_anomaly(pts, origin, radius_m, mode, grid=16):
61+
"""Grid-density anomaly over the point cloud (dependency-free, honest about
62+
being grid-density, not gaussian KDE). Returns (target_lat, target_lon, z)."""
63+
olat, olon = origin
64+
span = radius_m / EARTH_M_PER_DEG # half-extent in degrees lat
65+
cos_lat = math.cos(math.radians(olat)) or 1e-9
66+
span_lon = span / cos_lat
67+
68+
if mode == "blindspot":
69+
# plain random point — no anomaly search.
70+
target = pts[len(pts) // 2]
71+
return target[0], target[1], 0.0
72+
73+
# Bin into grid x grid cells; count per cell.
74+
counts: dict[tuple[int, int], int] = {}
75+
members: dict[tuple[int, int], list] = {}
76+
for (la, lo) in pts:
77+
gx = min(grid - 1, max(0, int((lo - (olon - span_lon)) / (2 * span_lon) * grid)))
78+
gy = min(grid - 1, max(0, int((la - (olat - span)) / (2 * span) * grid)))
79+
counts[(gx, gy)] = counts.get((gx, gy), 0) + 1
80+
members.setdefault((gx, gy), []).append((la, lo))
81+
82+
def cell_center(x: int, y: int) -> tuple[float, float]:
83+
return ((olat - span) + (y + 0.5) / grid * 2 * span,
84+
(olon - span_lon) + (x + 0.5) / grid * 2 * span_lon)
85+
86+
# Only cells whose centre is within the *circular* radius are candidates,
87+
# so the chosen point never escapes --radius (a square grid's corners do).
88+
in_circle = [
89+
(x, y) for x in range(grid) for y in range(grid)
90+
if haversine_m(olat, olon, *cell_center(x, y)) <= radius_m
91+
]
92+
all_cells = [counts.get(c, 0) for c in in_circle]
93+
mean = sum(all_cells) / len(all_cells)
94+
var = sum((c - mean) ** 2 for c in all_cells) / len(all_cells)
95+
std = math.sqrt(var) or 1e-9
96+
97+
if mode == "void":
98+
cell = min(in_circle, key=lambda c: counts.get(c, 0))
99+
tlat, tlon = cell_center(*cell)
100+
return tlat, tlon, (counts.get(cell, 0) - mean) / std
101+
102+
# attractor -> densest in-circle cell; target = centroid of its points.
103+
cell = max(in_circle, key=lambda c: counts.get(c, 0))
104+
z = (counts.get(cell, 0) - mean) / std
105+
mem = members.get(cell) or [cell_center(*cell)]
106+
tlat = sum(p[0] for p in mem) / len(mem)
107+
tlon = sum(p[1] for p in mem) / len(mem)
108+
if mode == "power":
109+
vcell = min(in_circle, key=lambda c: counts.get(c, 0))
110+
vz = (counts.get(vcell, 0) - mean) / std
111+
if abs(vz) > abs(z):
112+
tlat, tlon = cell_center(*vcell)
113+
return tlat, tlon, vz
114+
return tlat, tlon, z
115+
116+
117+
def haversine_m(lat1, lon1, lat2, lon2) -> float:
118+
r = 6_371_000.0
119+
p1, p2 = math.radians(lat1), math.radians(lat2)
120+
dp = math.radians(lat2 - lat1)
121+
dl = math.radians(lon2 - lon1)
122+
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
123+
return 2 * r * math.asin(min(1.0, math.sqrt(a)))
124+
125+
126+
def bearing_deg(lat1, lon1, lat2, lon2) -> float:
127+
p1, p2 = math.radians(lat1), math.radians(lat2)
128+
dl = math.radians(lon2 - lon1)
129+
x = math.sin(dl) * math.cos(p2)
130+
y = math.cos(p1) * math.sin(p2) - math.sin(p1) * math.cos(p2) * math.cos(dl)
131+
return (math.degrees(math.atan2(x, y)) + 360.0) % 360.0
132+
133+
134+
def compass_16(bearing: float) -> str:
135+
dirs = ["北", "北东北", "东北", "东东北", "东", "东东南", "东南", "南东南",
136+
"南", "南西南", "西南", "西西南", "西", "西西北", "西北", "北西北"]
137+
return dirs[int((bearing + 11.25) % 360 / 22.5)]
138+
139+
140+
def huangli_directions(date_str: str | None) -> dict:
141+
"""Today's 黄历 吉神方位 (八卦 -> bearing). Empty dict if lunar_python absent."""
142+
try:
143+
from datetime import datetime
144+
145+
from lunar_python import Solar # type: ignore
146+
if date_str:
147+
dt = datetime.strptime(date_str, "%Y-%m-%d")
148+
else:
149+
dt = datetime(2026, 1, 1) # deterministic default; pass --date for today
150+
lunar = Solar.fromYmdHms(dt.year, dt.month, dt.day, 12, 0, 0).getLunar()
151+
out = {}
152+
for name, method in (("喜神", "getDayPositionXi"), ("财神", "getDayPositionCai"),
153+
("福神", "getDayPositionFu")):
154+
try:
155+
gua = getattr(lunar, method)()
156+
if gua in BAGUA_BEARING:
157+
out[name] = {"卦": gua, "bearing": BAGUA_BEARING[gua],
158+
"compass": compass_16(BAGUA_BEARING[gua])}
159+
except Exception:
160+
continue
161+
return out
162+
except Exception:
163+
return {}
164+
165+
166+
def build_parser() -> argparse.ArgumentParser:
167+
p = argparse.ArgumentParser(description="今日随机寻访点 (QRNG 探索提示)")
168+
p.add_argument("--lat", type=float, required=True, help="当前纬度")
169+
p.add_argument("--lon", type=float, required=True, help="当前经度")
170+
p.add_argument("--radius", type=float, default=3000.0, help="半径(米), 默认 3000")
171+
p.add_argument("--mode", choices=["attractor", "void", "power", "blindspot"],
172+
default="attractor")
173+
p.add_argument("--points", type=int, default=10000, help="撒点数, 默认 10000")
174+
p.add_argument("--entropy", choices=["system", "quantum"], default="system")
175+
p.add_argument("--intention", type=str, default=None,
176+
help="意图(概念性, 不影响随机数; 仅记录)")
177+
p.add_argument("--date", type=str, default=None, help="黄历日期 YYYY-MM-DD")
178+
p.add_argument("--seed", type=int, default=None, help="确定性种子 (测试/复现)")
179+
return p
180+
181+
182+
def main(argv: list[str] | None = None) -> int:
183+
args = build_parser().parse_args(argv)
184+
if not (-90 <= args.lat <= 90 and -180 <= args.lon <= 180):
185+
json_print({"ok": False, "error": "invalid_coords"})
186+
return 1
187+
if not (50 <= args.radius <= 100_000):
188+
json_print({"ok": False, "error": "radius_out_of_range",
189+
"message": "radius 必须在 50-100000 米"})
190+
return 1
191+
n = max(100, min(50_000, args.points))
192+
193+
rng = entropy.get_rng(args.seed, args.entropy)
194+
pts = random_points(args.lat, args.lon, args.radius, n, rng)
195+
tlat, tlon, z = find_anomaly(pts, (args.lat, args.lon), args.radius, args.mode)
196+
197+
dist = haversine_m(args.lat, args.lon, tlat, tlon)
198+
brg = bearing_deg(args.lat, args.lon, tlat, tlon)
199+
200+
out = {
201+
"ok": True,
202+
"tool": "explore",
203+
"mode": args.mode,
204+
"intention": args.intention,
205+
"entropy": entropy.describe(rng, args.entropy, args.seed),
206+
"origin": {"lat": args.lat, "lon": args.lon},
207+
"target": {"lat": round(tlat, 6), "lon": round(tlon, 6)},
208+
"distance_m": round(dist, 1),
209+
"bearing_deg": round(brg, 1),
210+
"compass": compass_16(brg),
211+
"anomaly_z": round(z, 2),
212+
"huangli_directions": huangli_directions(args.date),
213+
"safety": SAFETY,
214+
"disclaimer": (
215+
"随机探索散步提示。attractor/void 是纯统计密度涨落, "
216+
"非预兆、非念力(MMI)影响; 意图仅记录不改变随机数。黄历方位为文化叠加。"
217+
"前往陌生地点有现实风险, 注意安全, 后果自负。"
218+
),
219+
}
220+
json_print(out)
221+
return 0
222+
223+
224+
if __name__ == "__main__":
225+
sys.exit(main())

scripts/ziwei_calc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
warn,
4343
)
4444

45-
VERSION = "1.1.8"
45+
VERSION = "1.1.9"
4646

4747

4848
# --------------------------------------------------------------------------- #

tests/test_explore.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Tests for explore_cast — QRNG exploration point (Randonautica-inspired).
2+
3+
All deterministic via --seed; no network (uses seed/system entropy).
4+
"""
5+
import json
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
10+
import explore_cast as ex
11+
import pytest
12+
13+
SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
14+
15+
16+
def run(*args) -> dict:
17+
proc = subprocess.run(
18+
[sys.executable, str(SCRIPTS / "explore_cast.py"), *map(str, args)],
19+
capture_output=True, text=True, encoding="utf-8",
20+
)
21+
return json.loads(proc.stdout)
22+
23+
24+
@pytest.mark.parametrize("mode", ["attractor", "void", "power", "blindspot"])
25+
def test_target_within_radius(mode):
26+
d = run("--lat", -33.8688, "--lon", 151.2093, "--radius", 3000,
27+
"--mode", mode, "--seed", 1)
28+
assert d["ok"] is True
29+
assert d["distance_m"] <= 3000.1, f"{mode} escaped radius: {d['distance_m']}"
30+
assert 0 <= d["bearing_deg"] <= 360
31+
32+
33+
def test_determinism_with_seed():
34+
a = run("--lat", -33.8688, "--lon", 151.2093, "--seed", 7)
35+
b = run("--lat", -33.8688, "--lon", 151.2093, "--seed", 7)
36+
assert a["target"] == b["target"]
37+
38+
39+
def test_entropy_and_safety_and_disclaimer_present():
40+
d = run("--lat", 1.0, "--lon", 1.0, "--seed", 3)
41+
assert d["entropy"]["source"] == "seed"
42+
assert isinstance(d["safety"], list) and len(d["safety"]) >= 3
43+
# Honest framing must be explicit — no MMI / prediction claims.
44+
assert "MMI" in d["disclaimer"] or "念力" in d["disclaimer"]
45+
assert "非预兆" in d["disclaimer"]
46+
47+
48+
def test_huangli_directions_with_date():
49+
d = run("--lat", -33.8688, "--lon", 151.2093, "--seed", 1, "--date", "2026-06-01")
50+
hd = d["huangli_directions"]
51+
assert "财神" in hd and "bearing" in hd["财神"]
52+
53+
54+
def test_invalid_coords_rejected():
55+
d = run("--lat", 200, "--lon", 0, "--seed", 1)
56+
assert d["ok"] is False and d["error"] == "invalid_coords"
57+
58+
59+
def test_radius_out_of_range_rejected():
60+
d = run("--lat", 0, "--lon", 0, "--radius", 10, "--seed", 1)
61+
assert d["ok"] is False and d["error"] == "radius_out_of_range"
62+
63+
64+
# --------------------------------------------------------------------------- #
65+
# Unit-level geometry checks
66+
# --------------------------------------------------------------------------- #
67+
68+
def test_random_points_within_radius():
69+
import random
70+
rng = random.Random(0)
71+
pts = ex.random_points(-33.0, 151.0, 1000.0, 500, rng)
72+
for la, lo in pts:
73+
assert ex.haversine_m(-33.0, 151.0, la, lo) <= 1000.5
74+
75+
76+
def test_bearing_cardinals():
77+
# due north / east of origin
78+
assert abs(ex.bearing_deg(0, 0, 1, 0) - 0.0) < 1.0
79+
assert abs(ex.bearing_deg(0, 0, 0, 1) - 90.0) < 1.0
80+
81+
82+
def test_compass_16_buckets():
83+
assert ex.compass_16(0) == "北"
84+
assert ex.compass_16(90) == "东"
85+
assert ex.compass_16(180) == "南"
86+
assert ex.compass_16(270) == "西"

0 commit comments

Comments
 (0)