|
| 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()) |
0 commit comments