271 lines
8.3 KiBLFS
Bash
271 lines
8.3 KiBLFS
Bash
#!/bin/bash
|
|
set -e
|
|
|
|
python3 - << 'EOF'
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
DATA_PATH = "/root/data.xlsx"
|
|
OUT_PATH = "/root/answer.txt"
|
|
|
|
# ----------------------------
|
|
# Robust helpers
|
|
# ----------------------------
|
|
def coerce_int_series(s: pd.Series) -> pd.Series:
|
|
"""
|
|
Coerce a Series to numeric (int-like) robustly:
|
|
- handles numbers
|
|
- handles strings with spaces / weird chars by extracting the first integer token
|
|
"""
|
|
as_str = s.astype(str).str.strip()
|
|
extracted = as_str.str.extract(r"(-?\d+)")[0]
|
|
return pd.to_numeric(extracted, errors="coerce")
|
|
|
|
def is_int_1_6(x) -> bool:
|
|
return np.isfinite(x) and (x % 1 == 0) and (1 <= x <= 6)
|
|
|
|
# ----------------------------
|
|
# 1) Load ALL sheets (as raw grid) and pick the sheet most likely containing the dice table
|
|
# ----------------------------
|
|
xls = pd.ExcelFile(DATA_PATH)
|
|
|
|
best = None # (strength, sheet_name, num_df)
|
|
for sh in xls.sheet_names:
|
|
raw = pd.read_excel(DATA_PATH, sheet_name=sh, header=None, dtype=object)
|
|
num = raw.apply(coerce_int_series)
|
|
|
|
scores = []
|
|
for c in num.columns:
|
|
vals = num[c].dropna()
|
|
if len(vals) < 50:
|
|
continue
|
|
frac = ((vals >= 1) & (vals <= 6) & ((vals % 1) == 0)).mean()
|
|
scores.append((float(frac), int(len(vals)), c))
|
|
|
|
scores.sort(reverse=True, key=lambda t: (t[0], t[1]))
|
|
if len(scores) >= 6:
|
|
strength = sum(t[0] for t in scores[:6])
|
|
if best is None or strength > best[0]:
|
|
best = (strength, sh, num)
|
|
|
|
if best is None:
|
|
raise RuntimeError(
|
|
"Cannot find a sheet with 6 dice-like columns. "
|
|
"Likely the file is missing Roll columns or they are not readable as numbers."
|
|
)
|
|
|
|
strength, sheet_name, num = best
|
|
|
|
# ----------------------------
|
|
# 2) Detect the 6 dice columns (top-6 by fraction in [1..6])
|
|
# (They do NOT need to be consecutive.)
|
|
# ----------------------------
|
|
col_scores = []
|
|
for c in num.columns:
|
|
vals = num[c].dropna()
|
|
if len(vals) < 50:
|
|
continue
|
|
frac = ((vals >= 1) & (vals <= 6) & ((vals % 1) == 0)).mean()
|
|
col_scores.append((float(frac), int(len(vals)), c))
|
|
|
|
col_scores.sort(reverse=True, key=lambda t: (t[0], t[1]))
|
|
dice_idx = [t[2] for t in col_scores[:6]]
|
|
dice_idx = sorted(dice_idx) # left-to-right
|
|
|
|
# ----------------------------
|
|
# 3) Detect the game-id column among remaining columns
|
|
# Heuristic: many values repeat exactly twice (each game has 2 turns)
|
|
# ----------------------------
|
|
def game_like_score(col_idx) -> float:
|
|
s = num[col_idx].dropna()
|
|
if len(s) < 200:
|
|
return -1.0
|
|
s = s[(s % 1) == 0].astype(int)
|
|
if len(s) < 200:
|
|
return -1.0
|
|
|
|
vc = pd.Series(s).value_counts()
|
|
frac_twice = float((vc == 2).mean()) if len(vc) else 0.0
|
|
frac_once = float((vc == 1).mean()) if len(vc) else 0.0
|
|
|
|
mn, mx = int(s.min()), int(s.max())
|
|
nunq = int(s.nunique())
|
|
|
|
sc = 0.0
|
|
sc += 1.0 if mn == 1 else 0.0
|
|
sc += 1.0 if 500 <= mx <= 20000 else 0.0
|
|
sc += 1.0 if 500 <= nunq <= 20000 else 0.0
|
|
sc += 4.0 * frac_twice
|
|
sc -= 1.5 * frac_once # penalize almost-unique columns (often Turn index)
|
|
return sc
|
|
|
|
other_cols = [c for c in num.columns if c not in dice_idx]
|
|
if not other_cols:
|
|
raise RuntimeError("No non-dice columns left to detect Game/Turn columns.")
|
|
|
|
game_col = max(other_cols, key=game_like_score)
|
|
game_sc = game_like_score(game_col)
|
|
if game_sc < 1.2:
|
|
raise RuntimeError(
|
|
f"Cannot detect Game-number column confidently (best_col={game_col}, score={game_sc:.3f}). "
|
|
"Ensure there is a Game number column where each id appears twice."
|
|
)
|
|
|
|
# ----------------------------
|
|
# 4) Detect turn column (optional; helps if you moved rows)
|
|
# ----------------------------
|
|
def turn_like_score(col_idx) -> float:
|
|
s = num[col_idx].dropna()
|
|
if len(s) < 200:
|
|
return -1.0
|
|
s = s[(s % 1) == 0].astype(int)
|
|
if len(s) < 200:
|
|
return -1.0
|
|
mn, mx = int(s.min()), int(s.max())
|
|
nunq = int(s.nunique())
|
|
uniq_ratio = nunq / max(1, len(s))
|
|
sc = 0.0
|
|
sc += 1.0 if mn == 1 else 0.0
|
|
sc += 1.0 if 2000 <= mx <= 20000 else 0.0
|
|
sc += 2.0 * uniq_ratio
|
|
return sc
|
|
|
|
turn_col = None
|
|
turn_candidates = [c for c in other_cols if c != game_col]
|
|
if turn_candidates:
|
|
tc = max(turn_candidates, key=turn_like_score)
|
|
if turn_like_score(tc) > 2.0:
|
|
turn_col = tc
|
|
|
|
# ----------------------------
|
|
# 5) Build clean turns table: (turn?), game, r1..r6
|
|
# ----------------------------
|
|
use_cols = []
|
|
if turn_col is not None:
|
|
use_cols.append(turn_col)
|
|
use_cols.append(game_col)
|
|
use_cols.extend(dice_idx)
|
|
|
|
sub = num[use_cols].copy()
|
|
|
|
names = (["turn"] if turn_col is not None else []) + ["game"] + [f"r{i+1}" for i in range(6)]
|
|
sub.columns = names
|
|
dice_cols = [f"r{i+1}" for i in range(6)]
|
|
|
|
# Filter valid dice rows
|
|
mask = sub[dice_cols].notna().all(axis=1)
|
|
for c in dice_cols:
|
|
mask &= sub[c].apply(is_int_1_6)
|
|
mask &= sub["game"].notna() & ((sub["game"] % 1) == 0)
|
|
|
|
sub = sub[mask].copy()
|
|
sub["game"] = sub["game"].astype(int)
|
|
for c in dice_cols:
|
|
sub[c] = sub[c].astype(int)
|
|
if "turn" in sub.columns:
|
|
sub["turn"] = sub["turn"].astype(int)
|
|
|
|
# ----------------------------
|
|
# 6) Force-insert the specific missing row if it was dropped during parsing
|
|
# Turn 15, Game 8, Rolls: 4 6 4 2 4 5
|
|
# ----------------------------
|
|
missing = {"turn": 15, "game": 8, "r1": 4, "r2": 6, "r3": 4, "r4": 2, "r5": 4, "r6": 5}
|
|
|
|
def ensure_missing_row(sub: pd.DataFrame) -> pd.DataFrame:
|
|
has_turn = "turn" in sub.columns
|
|
if has_turn:
|
|
m = (sub["turn"] == missing["turn"]) & (sub["game"] == missing["game"])
|
|
for c in dice_cols:
|
|
m &= (sub[c] == missing[c])
|
|
exists = bool(m.any())
|
|
else:
|
|
m = (sub["game"] == missing["game"])
|
|
for c in dice_cols:
|
|
m &= (sub[c] == missing[c])
|
|
exists = bool(m.any())
|
|
|
|
if not exists:
|
|
row = {k: missing[k] for k in (["turn", "game"] if has_turn else ["game"]) + dice_cols}
|
|
sub = pd.concat([sub, pd.DataFrame([row])], ignore_index=True)
|
|
return sub
|
|
|
|
sub = ensure_missing_row(sub)
|
|
|
|
# ----------------------------
|
|
# 7) Scoring
|
|
# ----------------------------
|
|
def turn_scores(turn):
|
|
mx = max(turn)
|
|
mn = min(turn)
|
|
high_often = mx * sum(1 for v in turn if v == mx)
|
|
summation = sum(turn)
|
|
highs_lows = mx * mn * (mx - mn)
|
|
only_two = 30 if len(set(turn)) == 2 else 0
|
|
all_numbers = 40 if set(turn) == {1, 2, 3, 4, 5, 6} else 0
|
|
|
|
def is_run4(a):
|
|
return (
|
|
(a[0] + 1 == a[1] and a[1] + 1 == a[2] and a[2] + 1 == a[3]) or
|
|
(a[0] - 1 == a[1] and a[1] - 1 == a[2] and a[2] - 1 == a[3])
|
|
)
|
|
|
|
run4 = 50 if any(is_run4(turn[i:i+4]) for i in range(3)) else 0
|
|
return np.array([high_often, summation, highs_lows, only_two, all_numbers, run4], dtype=int)
|
|
|
|
def best_game_score(s1, s2):
|
|
best = 0
|
|
for i in range(6):
|
|
for j in range(6):
|
|
if i == j:
|
|
continue
|
|
best = max(best, int(s1[i] + s2[j]))
|
|
return best
|
|
|
|
# Compute each game score by grouping on game id (robust to row reordering)
|
|
game_scores = {}
|
|
for gid, gdf in sub.groupby("game", sort=True):
|
|
if "turn" in gdf.columns:
|
|
gdf = gdf.sort_values("turn")
|
|
else:
|
|
gdf = gdf.sort_index()
|
|
|
|
gdf = gdf.head(2) # each game should have exactly 2 turns
|
|
if len(gdf) < 2:
|
|
continue
|
|
|
|
t1 = gdf.iloc[0][dice_cols].tolist()
|
|
t2 = gdf.iloc[1][dice_cols].tolist()
|
|
game_scores[int(gid)] = best_game_score(turn_scores(t1), turn_scores(t2))
|
|
|
|
if not game_scores:
|
|
raise RuntimeError("No game scores computed. Your edited file may be missing two turns per game.")
|
|
|
|
# ----------------------------
|
|
# 8) Q29
|
|
# ----------------------------
|
|
win_p1 = 0
|
|
win_p2 = 0
|
|
|
|
max_gid = max(game_scores.keys())
|
|
for m in range(1, max_gid // 2 + 1):
|
|
g1, g2 = 2*m - 1, 2*m
|
|
if g1 not in game_scores or g2 not in game_scores:
|
|
continue
|
|
if game_scores[g1] > game_scores[g2]:
|
|
win_p1 += 1
|
|
elif game_scores[g2] > game_scores[g1]:
|
|
win_p2 += 1
|
|
|
|
ans = win_p1 - win_p2
|
|
|
|
# ----------------------------
|
|
# 9) Write answer
|
|
# ----------------------------
|
|
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
|
f.write(str(ans) + "\n")
|
|
|
|
print(f"[oracle] sheet={sheet_name} dice_idx={dice_idx} game_col_idx={game_col} turn_col_idx={turn_col}")
|
|
print(f"[oracle] p1_wins={win_p1} p2_wins={win_p2} answer={ans}")
|
|
print(f"[oracle] wrote answer {ans} to {OUT_PATH}")
|
|
EOF
|