<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>舒尔特方格 · Schulte Grid</title>
<style>
  :root{
    /* 羊皮纸低对比配色，暖色、避免眩光 */
    --bg:#ece3cb;
    --panel:#f4ebd6;
    --cell:#f8f1df;
    --cell-done:#e6d9ba;
    --cell-border:#d7c69c;
    --ink:#58492f;
    --ink-dim:#8a7a58;
    --faded:#b6a178;
    --accent:#9c7b45;
    --danger:#b06a4b;
    --danger-soft:#d8a98c;
    --good:#7a8b6f;
    --mono:ui-monospace,"SF Mono","Menlo","Consolas","Roboto Mono","Courier New",monospace;
  }
  *{box-sizing:border-box; margin:0; padding:0; -webkit-tap-highlight-color:transparent;}
  html,body{height:100%;}
  body{background:var(--bg); color:var(--ink); font-family:'PingFang SC','Microsoft YaHei',system-ui,sans-serif;
       display:flex; align-items:flex-start; justify-content:center; padding:20px 12px 40px;}
  .wrap{width:min(96vw,700px);}

  .btn{flex:1; border:none; border-radius:8px; padding:11px 0; font-size:15px; cursor:pointer; font-family:inherit;}
  .btn:hover{filter:brightness(1.06);}
  .btn:active{transform:scale(.98);}

  /* ---- 主界面 ---- */
  header{display:flex; align-items:center; justify-content:space-between; margin-bottom:14px;}
  header h1{font-size:22px; letter-spacing:2px; font-weight:700;}
  .account-note{font-size:12px; color:var(--ink-dim); text-align:right;}

  .controls{display:flex; gap:8px; align-items:center; margin-bottom:10px; flex-wrap:wrap;}
  .mode{background:var(--panel); border:1px solid var(--cell-border); color:var(--ink-dim); border-radius:9px;
        padding:8px 14px; font-size:14px; cursor:pointer; font-family:inherit; font-variant-numeric:tabular-nums;}
  .mode:hover{border-color:var(--accent);}
  .mode.active{background:var(--accent); border-color:var(--accent); color:#fbf6e8;}
  .mode.active:hover{filter:brightness(1.05);}
  .btn.restart{margin-left:auto; background:var(--panel); border:1px solid var(--cell-border); color:var(--ink);
               border-radius:9px; padding:8px 16px; font-size:14px; cursor:pointer;}
  .btn.restart:hover{border-color:var(--accent); color:var(--accent);}

  /* 状态条 */
  .status{display:flex; gap:12px; justify-content:center; margin-bottom:10px; font-variant-numeric:tabular-nums;}
  .stat{background:var(--panel); border:1px solid var(--cell-border); border-radius:10px; padding:6px 14px; text-align:center; min-width:84px;}
  .stat .k{font-size:11px; color:var(--ink-dim);}
  .stat .v{font-family:var(--mono); font-size:18px; font-weight:600;}
  .stat .v.next{color:var(--accent); font-size:22px;}

  /* 方格 */
  .board{display:grid;
         grid-template-columns:repeat(var(--n),minmax(0,1fr));
         grid-template-rows:repeat(var(--n),minmax(0,1fr));
         gap:var(--gap);
         width:min(94vw,600px); margin:0 auto; aspect-ratio:1;}
  .board.s5{--gap:10px; --fs:clamp(20px,6.4vw,40px);}
  .board.s7{--gap:7px;  --fs:clamp(13px,3.6vw,23px);}
  .board.s10{--gap:5px; --fs:clamp(9px,2.5vw,15px);}
  .cell{background:var(--cell); border:1px solid var(--cell-border); border-radius:6px;
        display:flex; align-items:center; justify-content:center; width:100%; height:100%;
        font-family:var(--mono); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1,"zero" 1;
        font-size:var(--fs); font-weight:600; color:var(--ink); cursor:pointer;
        transition:opacity .3s, background .3s, color .3s, transform .08s;}
  .cell:hover{background:#fbf5e3;}
  .cell:active{transform:scale(.94);}
  .cell.done{color:var(--faded); background:var(--cell-done); opacity:.42; cursor:default;}
  .cell.flash{animation:flash .45s;}
  @keyframes flash{0%,100%{background:var(--cell);}45%{background:var(--danger-soft);}}
  .board.ended .cell{cursor:default;}

  /* 结果 */
  .result{text-align:center; margin-top:12px; min-height:52px;}
  .result .line{font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:20px; font-weight:700;}
  .result .line small{font-size:13px; color:var(--ink-dim); font-weight:400;}
  .result .newbest{color:var(--good); font-size:14px; margin-top:4px;}

  /* 记录 */
  .records{margin-top:20px;}
  .records h2{font-size:15px; letter-spacing:1px; margin-bottom:10px; color:var(--ink);}
  .rec-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px;}
  .rec-sec{background:var(--panel); border:1px solid var(--cell-border); border-radius:12px; padding:12px 14px;}
  .rec-sec h3{font-size:13px; color:var(--ink-dim); margin-bottom:8px; font-weight:600;}
  .rec-sec ol{list-style:none; counter-reset:r;}
  .rec-sec li{counter-increment:r; display:flex; align-items:center; gap:8px; font-size:13px; padding:3px 0;
             font-variant-numeric:tabular-nums;}
  .rec-sec li::before{content:counter(r); width:18px; height:18px; border-radius:4px; background:var(--cell-border);
                      color:#fbf6e8; font-size:11px; display:flex; align-items:center; justify-content:center; flex:none;}
  .rec-sec li b{font-family:var(--mono); font-weight:600;}
  .rec-sec li em{color:var(--ink-dim); font-style:normal; font-size:12px;}
  .rec-sec li i{color:var(--ink-dim); font-style:normal; font-size:11px; margin-left:auto;}
  .empty{color:var(--ink-dim); font-size:13px; padding:6px 0;}

  /* 洗乱提示 */
  .toast{position:fixed; left:50%; top:20%; transform:translateX(-50%); background:rgba(90,72,45,.94); color:#fbf6e8;
         padding:10px 20px; border-radius:10px; font-size:15px; letter-spacing:1px; opacity:0; pointer-events:none;
         transition:opacity .25s; z-index:40;}
  .toast.show{opacity:1;}
</style>
</head>
<body>

<!-- 主界面 -->
<div class="wrap" id="app">
  <header>
    <h1>舒尔特方格</h1>
    <div class="account-note">本机记录自动保存<br>网站账号用于全站排行</div>
  </header>

  <div class="controls">
    <button class="mode active" data-mode="5x5">5×5</button>
    <button class="mode" data-mode="7x7">7×7</button>
    <button class="mode" data-mode="10x10">10×10</button>
    <button class="mode" data-mode="hard">困难</button>
    <button class="mode" data-mode="hell">地狱魔</button>
    <button class="btn restart" id="restartBtn">重开一局</button>
  </div>

  <div class="status">
    <div class="stat"><div class="k">时间</div><div class="v" id="timer">0.00s</div></div>
    <div class="stat"><div class="k">下一个</div><div class="v next" id="next">1</div></div>
    <div class="stat"><div class="k">错点</div><div class="v" id="errors">0</div></div>
  </div>

  <div class="board s5" id="board" hidden></div>
  <div class="result" id="result"></div>

  <div class="records">
    <h2>我的记录 · 各模式最佳</h2>
    <div class="rec-grid" id="records"></div>
  </div>
</div>

<script src="/blog/assets/leaderboard.js"></script>
<script>
const MODES = { "5x5":25, "7x7":49, "10x10":100, "hard":25, "hell":25 };
const RECORDS_KEY = "schulte-records-v2";
const LEGACY_DB_KEY = "schulte-users";
const LEGACY_SESSION_KEY = "schulte-session";
const KEEP = 10;

let records = {};
let state = { mode:"5x5", nums:[], pos:{}, next:1, errors:0, startTime:null, timerId:null, finished:false };

AdnovelArena.init({
  game: "schulte",
  defaultMode: "5x5",
  modes: { "5x5":"5×5", "7x7":"7×7", "10x10":"10×10", hard:"困难", hell:"地狱魔" }
});

const $ = id => document.getElementById(id);

/* ---------- 本机记录：合并旧账号成绩并清除旧明文密码 ---------- */
function loadRecords(){
  let merged = {};
  try{
    const saved = JSON.parse(localStorage.getItem(RECORDS_KEY));
    if(saved && typeof saved === "object") merged = saved;
    else{
      const legacy = JSON.parse(localStorage.getItem(LEGACY_DB_KEY)) || {};
      Object.values(legacy).forEach(user=>{
        const oldRecords = user && user.records && typeof user.records === "object" ? user.records : {};
        Object.entries(oldRecords).forEach(([mode, list])=>{
          if(!MODES[mode] || !Array.isArray(list)) return;
          merged[mode] = (merged[mode] || []).concat(list.filter(item=>item && Number.isFinite(Number(item.t))));
        });
      });
      Object.keys(merged).forEach(mode=>{
        merged[mode].sort((a,b)=>Number(a.t)-Number(b.t));
        merged[mode] = merged[mode].slice(0, KEEP);
      });
      localStorage.setItem(RECORDS_KEY, JSON.stringify(merged));
    }
    localStorage.removeItem(LEGACY_DB_KEY);
    localStorage.removeItem(LEGACY_SESSION_KEY);
  }catch(e){}
  return merged;
}
function saveRecords(){ try{ localStorage.setItem(RECORDS_KEY, JSON.stringify(records)); }catch(e){} }

/* ---------- 工具 ---------- */
function shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
function fmt(ms){
  const s = ms/1000;
  if(s < 60) return s.toFixed(2)+"s";
  const m = Math.floor(s/60), r = s - m*60;
  return m + ":" + r.toFixed(2).padStart(5,"0");
}
function shortDate(iso){ const d = new Date(iso); return (d.getMonth()+1)+"-"+d.getDate()+" "+String(d.getHours()).padStart(2,"0")+":"+String(d.getMinutes()).padStart(2,"0"); }

/* ---------- 游戏 ---------- */
function newGame(){
  clearInterval(state.timerId);
  const n = MODES[state.mode];
  state.nums = shuffle(Array.from({length:n}, (_,i)=>i+1));
  state.next = 1; state.errors = 0; state.startTime = null; state.finished = false;
  state.pos = {};
  const b = $("board");
  b.hidden = false;
  b.className = "board s" + Math.sqrt(n);
  b.style.setProperty("--n", Math.sqrt(n));
  b.innerHTML = "";
  for(let i=0;i<n;i++){
    const num = state.nums[i];
    const cell = document.createElement("div");
    cell.className = "cell";
    cell.dataset.num = num;
    cell.textContent = num;
    state.pos[num] = cell;
    cell.addEventListener("click", ()=>onClick(cell));
    b.appendChild(cell);
  }
  $("timer").textContent = "0.00s";
  $("result").innerHTML = "";
  updateStatus();
}

function onClick(cell){
  if(state.finished) return;
  if(!state.startTime){
    state.startTime = performance.now();
    state.timerId = setInterval(updateTimer, 50);
    AdnovelArena.start(state.mode);
  }
  const num = parseInt(cell.dataset.num, 10);
  if(num === state.next){
    // 困难/地狱魔：点中后无视觉变化，靠记忆
    if(state.mode === "5x5" || state.mode === "7x7" || state.mode === "10x10"){
      cell.classList.add("done");
    }
    state.next++;
    if(state.next > MODES[state.mode]){ finish(); return; }
    if(state.mode === "hell" && (state.next-1) % 5 === 0){
      shufflePositions();
      toast("♻ 位置已洗乱！");
    }
  } else {
    state.errors++;
    cell.classList.remove("flash");
    void cell.offsetWidth;
    cell.classList.add("flash");
  }
  updateStatus();
}

/* 地狱魔：把还没点到的数字随机重排到剩余格子里 */
function shufflePositions(){
  const done = new Set();
  for(let k=1;k<state.next;k++) done.add(k);
  const cells = Array.from($("board").children);
  const spots = cells.filter(c => !done.has(parseInt(c.dataset.num, 10)));
  const nums = spots.map(c => parseInt(c.dataset.num, 10));
  shuffle(nums);
  spots.forEach((cell, i)=>{
    cell.dataset.num = nums[i];
    cell.textContent = nums[i];
    state.pos[nums[i]] = cell;
  });
}

/* 短暂的全局提示（如洗乱） */
let toastTimer = null;
function toast(msg){
  let t = document.querySelector(".toast");
  if(!t){ t = document.createElement("div"); t.className = "toast"; document.body.appendChild(t); }
  t.textContent = msg;
  t.classList.add("show");
  clearTimeout(toastTimer);
  toastTimer = setTimeout(()=>t.classList.remove("show"), 1200);
}

function updateTimer(){
  if(state.startTime) $("timer").textContent = fmt(performance.now() - state.startTime);
}
function updateStatus(){
  $("next").textContent = state.next;
  $("errors").textContent = state.errors;
}

function finish(){
  state.finished = true;
  clearInterval(state.timerId);
  $("board").classList.add("ended");
  const elapsed = performance.now() - state.startTime;
  $("timer").textContent = fmt(elapsed);
  AdnovelArena.submit({
    mode: state.mode,
    score: state.errors,
    durationMs: Math.round(elapsed),
    meta: { errors: state.errors }
  });
  const best = saveRecord(state.mode, elapsed, state.errors);
  const recs = records[state.mode] || [];
  const rank = recs.findIndex(r => r.t === Math.round(elapsed)) + 1;
  $("result").innerHTML =
    `<div class="line">${fmt(elapsed)} <small>· 错点 ${state.errors}</small></div>` +
    (best ? `<div class="newbest">★ 新纪录！当前排名第 ${rank}</div>` : "");
}

/* ---------- 记录 ---------- */
function saveRecord(mode, time, errors){
  records[mode] = records[mode] || [];
  const t = Math.round(time);
  records[mode].push({ t, err:errors, d:new Date().toISOString() });
  records[mode].sort((a,b)=>a.t-b.t);
  records[mode] = records[mode].slice(0, KEEP);
  saveRecords();
  renderRecords();
  return records[mode].length > 0 && records[mode][0].t === t;
}

function renderRecords(){
  const box = $("records");
  box.innerHTML = "";
  const labels = { "5x5":"5×5", "7x7":"7×7", "10x10":"10×10", "hard":"困难", "hell":"地狱魔" };
  for(const key of ["5x5","7x7","10x10","hard","hell"]){
    const recs = records[key] || [];
    const sec = document.createElement("div");
    sec.className = "rec-sec";
    let html = `<h3>${labels[key] || key} · 最佳 ${Math.min(recs.length,5)}/${KEEP} 次</h3>`;
    if(!recs.length) html += `<p class="empty">暂无记录，来一局吧</p>`;
    else{
      html += "<ol>";
      recs.slice(0,5).forEach(r=>{
        html += `<li><b>${fmt(r.t)}</b><em>错${r.err}</em><i>${shortDate(r.d)}</i></li>`;
      });
      html += "</ol>";
    }
    sec.innerHTML = html;
    box.appendChild(sec);
  }
}

/* ---------- 模式切换 ---------- */
document.querySelectorAll(".mode").forEach(btn=>{
  btn.addEventListener("click", ()=>{
    document.querySelectorAll(".mode").forEach(x=>x.classList.remove("active"));
    btn.classList.add("active");
    state.mode = btn.dataset.mode;
    AdnovelArena.setMode(state.mode);
    newGame();
  });
});
$("restartBtn").addEventListener("click", newGame);

/* ---------- 启动 ---------- */
(function init(){
  records = loadRecords();
  renderRecords();
  newGame();
})();
</script>
</body>
</html>
