お宝の数字が1つだけかくれています。手がかりを1つずつ読んで、ちがう数字を自分でタップして消しながら、調査団の一員になってお宝の数字を見つけ出そう!
出題者になって、ひみつの数字と手がかりを自分で選ぼう。1つにしぼれる組み合わせができたら、合言葉が作れるよ。といてほしい人につたえよう!
この合言葉を、といてほしい人につたえよう!
ともだちからもらった合言葉を入力して、ちょうせんしよう!
チームごとに じゅんばんに ちょうせんして、手がかりの数を くらべよう!
じゅんびができたら、下のボタンをおしてね
手がかりの数が すくないほど 上い!(同じ数なら タイムが はやい方が上い)
③ README.txt の手順に沿って script.google.com に貼り付けてください。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>お宝発見!すうじ調査団</title>
<style>
:root{
--cream:#FBF6EC;
--ink:#2E3440;
--navy:#3B4A6B;
--navy-dark:#2A3652;
--gold:#E8B84B;
--gold-dark:#C79626;
--leaf:#6FA96B;
--leaf-dark:#4F8A4B;
--red:#C0392B;
--panel:#FFFFFF;
--line:#D9D2C2;
}
*{box-sizing:border-box;}
html,body{
margin:0; padding:0; height:100%;
font-family:"Hiragino Maru Gothic ProN","Yu Gothic",-apple-system,sans-serif;
background:var(--cream);
color:var(--ink);
overflow:hidden;
}
#app{ width:100vw; height:100vh; position:relative; }
#title-screen{
position:absolute; inset:0;
display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:18px; text-align:center; padding:24px; overflow-y:auto;
background:
radial-gradient(circle at 20% 20%, #EDEBFA 0%, transparent 45%),
radial-gradient(circle at 80% 80%, #FFF6E4 0%, transparent 45%),
var(--cream);
}
#title-screen h1{
font-size:clamp(24px, 5vw, 38px);
margin:0;
letter-spacing:.03em;
color:var(--navy);
text-shadow: 3px 3px 0 #fff;
}
#title-screen .lead{ font-size:13px; color:#6B6A62; max-width:44ch; line-height:1.8; margin:0 0 6px; }
.setting-block{ display:flex; flex-direction:column; gap:8px; align-items:center; }
.setting-label{ font-size:12.5px; font-weight:700; color:#475569; }
.btn-row{ display:flex; gap:10px; flex-wrap:wrap; justify-content:center; }
.opt-btn{
font-size:14px; font-weight:700; padding:10px 20px;
border:3px solid var(--line); border-radius:14px;
background:#fff; color:var(--ink);
cursor:pointer; transition:all 0.15s ease-out;
}
.opt-btn.active{ border-color:var(--navy); background:#EDEBFA; color:var(--navy); }
#start-btn{
margin-top:8px;
font-size:22px; font-weight:700; padding:14px 48px;
border:none; border-radius:999px;
background:var(--red); color:#fff;
box-shadow:0 6px 0 #8f2c20;
cursor:pointer; transition:all 0.15s ease-out;
}
#start-btn:active{ transform:translateY(6px); box-shadow:0 0 0 #8f2c20; }
#glossary-open-btn, #analysis-open-btn, #team-open-btn{
font-size:12.5px; font-weight:700; padding:8px 16px;
border:2px solid var(--gold-dark); border-radius:999px;
background:#fff7ea; color:var(--gold-dark);
cursor:pointer; transition:all 0.15s ease-out;
}
#analysis-open-btn{ border-color:var(--leaf-dark); background:#eaf6e8; color:var(--leaf-dark); }
#team-open-btn{ border-color:var(--navy); background:#EDEBFA; color:var(--navy); }
#team-setup-screen{
position:absolute; inset:0;
display:flex; align-items:center; justify-content:center;
padding:20px; overflow-y:auto;
background:
radial-gradient(circle at 20% 20%, #EDEBFA 0%, transparent 45%),
radial-gradient(circle at 80% 80%, #FFF6E4 0%, transparent 45%),
var(--cream);
}
#team-setup-card{
width:100%; max-width:420px; text-align:center;
background:#fff; border-radius:18px; padding:26px 22px;
box-shadow:0 10px 30px rgba(15,23,42,0.08);
display:flex; flex-direction:column; align-items:center; gap:14px;
}
#team-setup-card h2{ font-size:19px; margin:0; color:var(--navy); }
#team-setup-card .lead{ font-size:12.5px; color:#6B6A62; line-height:1.7; margin:0; }
#teamNameInputs{ width:100%; display:flex; flex-direction:column; gap:8px; }
.team-name-input{
width:100%; padding:10px 12px; font-size:14px; font-weight:700;
border-radius:10px; border:2px solid var(--line); font-family:inherit;
text-align:center;
}
.team-name-input:focus{ outline:none; border-color:var(--navy); }
#team-start-btn{
font-size:18px; font-weight:700; padding:13px 40px;
border:none; border-radius:999px;
background:var(--red); color:#fff;
box-shadow:0 5px 0 #8f2c20;
cursor:pointer; transition:all 0.15s ease-out;
}
#team-start-btn:active{ transform:translateY(5px); box-shadow:0 0 0 #8f2c20; }
#team-cancel-btn{
font-size:12.5px; font-weight:700; padding:8px 16px; border:none; border-radius:999px;
background:#e2e8f0; color:#334155; cursor:pointer; transition:all 0.15s ease-out;
}
#work-screen{
position:absolute; inset:0;
display:flex; flex-direction:column;
background:var(--cream);
}
header{
flex:0 0 auto;
display:flex; align-items:center; justify-content:space-between;
padding:9px 16px;
background:var(--panel); border-bottom:3px solid var(--line);
}
header .title{ font-size:15px; font-weight:700; color:var(--navy); }
header .timer{ font-size:14px; font-weight:700; color:var(--gold-dark); font-variant-numeric: tabular-nums; }
header .icon-btn{
font-size:12px; font-weight:700; padding:7px 12px;
border:none; border-radius:999px;
background:#e2e8f0; color:#334155; cursor:pointer; transition:all 0.15s ease-out;
}
#main-area{
flex:1 1 auto; min-height:0;
display:flex; gap:10px;
padding:10px;
overflow:hidden;
}
#grid-panel{ flex:1 1 0; min-width:0; display:flex; flex-direction:column; }
#remaining-count{ font-size:12.5px; color:#475569; margin-bottom:6px; text-align:center; }
#grid-scroll{ flex:1 1 auto; overflow-y:auto; }
#numGrid{
display:grid;
grid-template-columns:repeat(10, 1fr);
gap:4px;
}
.num-cell{
aspect-ratio:1/1;
display:flex; align-items:center; justify-content:center;
font-size:clamp(11px, 2vmin, 15px); font-weight:700;
background:#fff; border:2px solid var(--line); border-radius:8px;
cursor:pointer; transition:all 0.15s ease-out;
color:var(--ink);
}
.num-cell:hover{ border-color:var(--navy); }
.num-cell.eliminated{
background:#f1f5f9; color:#cbd5e1; border-color:#e2e8f0;
text-decoration:line-through;
}
.num-cell.lone-candidate{
border-color:var(--gold-dark); background:#fff7ea;
animation:pulse 1s infinite;
}
@keyframes pulse{
0%,100%{ box-shadow:0 0 0 0 rgba(199,150,38,0.5); }
50%{ box-shadow:0 0 0 6px rgba(199,150,38,0); }
}
#clue-panel{
flex:0 0 240px;
display:flex; flex-direction:column;
background:var(--panel); border:2px solid var(--line); border-radius:14px;
padding:12px; overflow:hidden;
}
#clue-panel h2{ font-size:13px; margin:0 0 8px; color:var(--navy); }
#clueList{ flex:1 1 auto; overflow-y:auto; font-size:13px; line-height:1.7; }
#clueList li{ margin-bottom:6px; padding:6px 8px; background:#EDEBFA; border-radius:8px; }
#next-clue-btn{
margin-top:8px;
font-size:13px; font-weight:700; padding:10px; border:none; border-radius:10px;
background:var(--navy); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
}
#next-clue-btn:disabled{ background:#cbd5e1; cursor:default; }
#bottom-bar{
flex:0 0 auto;
display:flex; align-items:center; justify-content:center; gap:10px;
padding:10px 16px;
background:var(--panel); border-top:3px solid var(--line);
}
#solve-btn{
font-size:15px; font-weight:700; padding:12px 26px; border:none; border-radius:999px;
background:var(--red); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
box-shadow:0 4px 0 #8f2c20;
}
#solve-btn:disabled{ background:#cbd5e1; box-shadow:none; cursor:default; }
#solve-btn:active:not(:disabled){ transform:translateY(4px); box-shadow:0 0 0 #8f2c20; }
#solveMsg{ font-size:13px; font-weight:700; min-height:18px; }
#analysis-screen{
position:absolute; inset:0;
display:flex; flex-direction:column;
background:var(--cream);
}
#analysis-controls{
flex:0 0 auto;
display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:center;
padding:12px 16px;
background:var(--panel); border-bottom:3px solid var(--line);
}
.mode-tabs{ display:flex; gap:6px; }
.tab-btn{
font-size:12.5px; font-weight:700; padding:8px 14px;
border:2px solid var(--line); border-radius:999px;
background:#fff; color:var(--ink);
cursor:pointer; transition:all 0.15s ease-out;
}
.tab-btn.active{ border-color:var(--navy); background:#EDEBFA; color:var(--navy); }
.num-pickers{ display:flex; gap:10px; align-items:center; }
.num-pickers label{ font-size:12.5px; font-weight:700; color:#475569; display:flex; align-items:center; gap:6px; }
.num-pickers input{
width:64px; padding:7px 8px; font-size:14px; font-weight:700; text-align:center;
border-radius:8px; border:2px solid var(--line); font-family:inherit;
}
.num-pickers input:focus{ outline:none; border-color:var(--navy); }
#analysis-summary{
flex:0 0 auto;
padding:10px 16px; text-align:center; font-size:13px; font-weight:700; color:var(--navy);
background:#EDEBFA; border-bottom:2px solid var(--line);
line-height:1.7;
}
#analysis-grid-wrap{ flex:1 1 auto; overflow-y:auto; padding:10px 14px; }
#analysisGrid{
display:grid;
grid-template-columns:repeat(10, 1fr);
gap:4px;
}
.analysis-cell{
aspect-ratio:1/1;
display:flex; align-items:center; justify-content:center;
font-size:clamp(11px, 2vmin, 14px); font-weight:700;
background:#fff; border:2px solid var(--line); border-radius:8px;
color:#94a3b8;
}
.analysis-cell.markA{ background:#DBEAFE; border-color:#60A5FA; color:#1D4ED8; }
.analysis-cell.markB{ background:#FED7AA; border-color:#F59E0B; color:#B45309; }
.analysis-cell.common{ background:#E9D5FF; border-color:#A855F7; color:#6B21A8; }
.analysis-cell.common.best{ box-shadow:0 0 0 3px #A855F7; }
#done-overlay{
position:absolute; inset:0; display:none;
align-items:center; justify-content:center;
background:rgba(46,52,64,0.55);
}
#done-overlay.show{ display:flex; }
#done-card{
background:#fff; border-radius:18px; padding:28px 26px; text-align:center;
max-width:340px; box-shadow:0 12px 30px rgba(0,0,0,0.2);
}
#done-card h2{ font-size:22px; margin:0 0 10px; color:var(--leaf-dark); }
#done-card p{ font-size:13.5px; color:#475569; line-height:1.7; margin:0 0 16px; }
#done-card button{
font-size:15px; font-weight:700; padding:11px 24px; border:none; border-radius:999px;
background:var(--navy); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
}
#done-send-row{
display:flex; gap:8px; margin-bottom:12px;
}
#done-name-input{
flex:1 1 auto;
padding:9px 12px; font-size:14px; border-radius:10px; border:2px solid var(--line);
font-family:inherit;
}
#done-name-input:focus{ outline:none; border-color:var(--navy); box-shadow:0 0 0 3px #EDEBFA; }
#done-send-btn{
flex-shrink:0;
font-size:13px; font-weight:700; padding:10px 16px; border:none; border-radius:10px;
background:var(--gold-dark); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
}
#done-send-btn:disabled{ background:#cbd5e1; cursor:default; }
#done-send-status{ font-size:12px; font-weight:700; min-height:16px; margin-bottom:8px; }
#team-transition-overlay, #team-leaderboard-overlay{
position:absolute; inset:0; display:none;
align-items:center; justify-content:center; padding:20px;
background:rgba(46,52,64,0.55);
}
#team-transition-overlay.show, #team-leaderboard-overlay.show{ display:flex; }
#team-transition-card, #team-leaderboard-card{
background:#fff; border-radius:18px; padding:28px 26px; text-align:center;
max-width:360px; width:100%;
box-shadow:0 12px 30px rgba(0,0,0,0.2);
}
#team-transition-card h2{ font-size:20px; margin:0 0 10px; color:var(--navy); }
#team-transition-card p{ font-size:13px; color:#475569; margin:0 0 16px; }
#team-transition-start{
font-size:16px; font-weight:700; padding:12px 26px; border:none; border-radius:999px;
background:var(--red); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
box-shadow:0 4px 0 #8f2c20;
}
#team-transition-start:active{ transform:translateY(4px); box-shadow:0 0 0 #8f2c20; }
#team-leaderboard-card h2{ font-size:20px; margin:0 0 6px; color:var(--gold-dark); }
#team-leaderboard-card .sub-note{ font-size:11.5px; color:#94a3b8; margin:0 0 14px; line-height:1.6; }
#teamLeaderboardList{
text-align:left; margin:0 0 16px; padding:0 0 0 22px; font-size:14px; line-height:2;
color:var(--ink);
}
#teamLeaderboardList li{ margin-bottom:2px; }
#teamLeaderboardList li:first-child{ font-weight:700; color:var(--gold-dark); }
#team-finish-btn{
font-size:15px; font-weight:700; padding:11px 24px; border:none; border-radius:999px;
background:var(--navy); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
}
#glossary-overlay{
position:absolute; inset:0; display:none;
align-items:center; justify-content:center; padding:20px;
background:rgba(46,52,64,0.55);
}
#glossary-overlay.show{ display:flex; }
#glossary-card{
background:#fff; border-radius:18px; padding:22px; max-width:480px; width:100%;
max-height:80vh; overflow-y:auto;
box-shadow:0 12px 30px rgba(0,0,0,0.2);
}
#glossary-card h2{ font-size:17px; margin:0 0 14px; color:var(--navy); text-align:center; }
.term-item{ margin-bottom:14px; }
.term-item .term{ font-size:15px; font-weight:700; color:var(--navy); margin-bottom:2px; }
.term-item .def{ font-size:13px; color:#475569; line-height:1.7; }
.term-item .ex{ font-size:12px; color:#94a3b8; margin-top:2px; }
#glossary-close{
display:block; margin:12px auto 0;
font-size:13px; font-weight:700; padding:9px 20px; border:none; border-radius:999px;
background:#e2e8f0; color:#334155; cursor:pointer; transition:all 0.15s ease-out;
}
#challenge-open-btn, #challenge-receive-open-btn{
font-size:12.5px; font-weight:700; padding:8px 16px;
border:2px solid var(--red); border-radius:999px;
background:#fdf0ee; color:var(--red);
cursor:pointer; transition:all 0.15s ease-out;
}
#challenge-receive-open-btn{ border-color:var(--gold-dark); background:#fff7ea; color:var(--gold-dark); }
#challenge-create-screen, #challenge-receive-screen{
position:absolute; inset:0;
display:flex; align-items:center; justify-content:center;
padding:20px; overflow-y:auto;
background:
radial-gradient(circle at 20% 20%, #EDEBFA 0%, transparent 45%),
radial-gradient(circle at 80% 80%, #FFF6E4 0%, transparent 45%),
var(--cream);
}
.challenge-card{
width:100%; max-width:440px; text-align:center;
background:#fff; border-radius:18px; padding:26px 22px;
box-shadow:0 10px 30px rgba(15,23,42,0.08);
display:flex; flex-direction:column; align-items:center; gap:14px;
}
.challenge-card h2{ font-size:19px; margin:0; color:var(--navy); }
.challenge-card .lead{ font-size:12.5px; color:#6B6A62; line-height:1.7; margin:0; }
#challengeSecretInput, #challengeReceiveInput{
width:100%; max-width:220px; padding:11px 14px; font-size:16px; font-weight:700;
border-radius:10px; border:2px solid var(--line); font-family:inherit; text-align:center;
}
#challengeSecretInput:focus, #challengeReceiveInput:focus{ outline:none; border-color:var(--navy); box-shadow:0 0 0 3px #EDEBFA; }
#challengeClueSection{ width:100%; display:flex; flex-direction:column; gap:8px; align-items:center; }
#challengeClueChecks{ width:100%; display:flex; flex-direction:column; gap:6px; text-align:left; }
.clue-check-row{
display:flex; align-items:center; gap:8px;
padding:8px 12px; border-radius:10px; border:2px solid var(--line); background:#fafafa;
cursor:pointer; transition:all 0.15s ease-out;
}
.clue-check-row:hover{ border-color:var(--navy); }
.clue-check-row input{ flex-shrink:0; width:16px; height:16px; cursor:pointer; transition:all 0.15s ease-out; }
.clue-check-row span{ font-size:13px; font-weight:700; color:var(--ink); }
#challengeStatus, #challengeReceiveStatus{ font-size:12.5px; font-weight:700; min-height:18px; }
#challengeMakeBtn, #challengeReceiveBtn{
font-size:16px; font-weight:700; padding:12px 30px; border:none; border-radius:999px;
background:var(--red); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
box-shadow:0 4px 0 #8f2c20;
}
#challengeReceiveBtn{ background:var(--navy); box-shadow:0 4px 0 #232e47; }
#challengeMakeBtn:disabled{ background:#cbd5e1; box-shadow:none; cursor:default; }
#challengeMakeBtn:active:not(:disabled), #challengeReceiveBtn:active{ transform:translateY(4px); box-shadow:0 0 0 #8f2c20; }
#challengeCreateBackBtn, #challengeReceiveBackBtn{
font-size:12.5px; font-weight:700; padding:8px 16px; border:none; border-radius:999px;
background:#e2e8f0; color:#334155; cursor:pointer; transition:all 0.15s ease-out;
}
#challengeResult{ width:100%; display:flex; flex-direction:column; gap:10px; align-items:center; }
#challengeResult p{ font-size:12.5px; color:#475569; margin:0; line-height:1.7; }
#challengeCode{
width:100%; word-break:break-all; font-family:monospace; font-size:12px; font-weight:700;
background:#f1f5f9; border:2px dashed var(--line); border-radius:10px; padding:12px;
color:var(--navy);
}
#challengeCopyBtn{
font-size:13px; font-weight:700; padding:10px 20px; border:none; border-radius:10px;
background:var(--gold-dark); color:#fff; cursor:pointer; transition:all 0.15s ease-out;
}
button:focus-visible, [tabindex]:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
[hidden]{ display:none !important; }
</style>
</head>
<body>
<div id="app">
<section id="title-screen">
<h1>お宝発見!<br>すうじ調査団</h1>
<p class="lead">お宝の数字が1つだけかくれています。手がかりを1つずつ読んで、ちがう数字を自分でタップして消しながら、調査団の一員になってお宝の数字を見つけ出そう!</p>
<div class="setting-block">
<span class="setting-label">はんい</span>
<div class="btn-row" id="rangeRow">
<button class="opt-btn active" data-range="50" type="button">1〜50</button>
<button class="opt-btn" data-range="100" type="button">1〜100</button>
</div>
</div>
<div class="setting-block">
<span class="setting-label">つかう手がかり(きほんの手がかりは いつも入ります)</span>
<div class="btn-row" id="unitRow">
<button class="opt-btn active" data-unit="evenodd" type="button">偶数・奇数</button>
<button class="opt-btn active" data-unit="multiple" type="button">倍数</button>
<button class="opt-btn active" data-unit="divisor" type="button">約数</button>
</div>
</div>
<button id="start-btn">はじめる</button>
<div class="btn-row">
<button id="team-open-btn" type="button">👥 グループ対決</button>
<button id="analysis-open-btn" type="button">🔬 重ねて分析</button>
<button id="glossary-open-btn" type="button">📖 用語辞典を見る</button>
</div>
<div class="btn-row">
<button id="challenge-open-btn" type="button">✏️ 挑戦状づくり</button>
<button id="challenge-receive-open-btn" type="button">🔑 合言葉で挑戦する</button>
</div>
</section>
<section id="challenge-create-screen" hidden>
<div class="challenge-card">
<h2>✏️ 挑戦状づくり</h2>
<p class="lead">出題者になって、ひみつの数字と手がかりを自分で選ぼう。1つにしぼれる組み合わせができたら、合言葉が作れるよ。といてほしい人につたえよう!</p>
<div class="setting-block">
<span class="setting-label">はんい</span>
<div class="btn-row" id="challengeRangeRow">
<button class="opt-btn active" data-range="50" type="button">1〜50</button>
<button class="opt-btn" data-range="100" type="button">1〜100</button>
</div>
</div>
<div class="setting-block">
<span class="setting-label">ひみつの数字</span>
<input type="number" id="challengeSecretInput" min="1" max="50" placeholder="数字を入力">
</div>
<div class="setting-block" id="challengeClueSection" hidden>
<span class="setting-label">つかう手がかりを選ぼう(けたの数は自動で入ります)</span>
<div id="challengeClueChecks"></div>
<div id="challengeStatus"></div>
</div>
<button id="challengeMakeBtn" type="button" disabled>🔑 合言葉をつくる</button>
<button id="challengeCreateBackBtn" type="button">タイトルへもどる</button>
<div id="challengeResult" hidden>
<p>この合言葉を、といてほしい人につたえよう!</p>
<div id="challengeCode"></div>
<button id="challengeCopyBtn" type="button">📋 コピーする</button>
</div>
</div>
</section>
<section id="challenge-receive-screen" hidden>
<div class="challenge-card">
<h2>🔑 合言葉で挑戦する</h2>
<p class="lead">ともだちからもらった合言葉を入力して、ちょうせんしよう!</p>
<input type="text" id="challengeReceiveInput" placeholder="合言葉を入力してね">
<div id="challengeReceiveStatus"></div>
<button id="challengeReceiveBtn" type="button">といてみる</button>
<button id="challengeReceiveBackBtn" type="button">タイトルへもどる</button>
</div>
</section>
<section id="team-setup-screen" hidden>
<div id="team-setup-card">
<h2>👥 グループ対決</h2>
<p class="lead">チームごとに じゅんばんに ちょうせんして、手がかりの数を くらべよう!</p>
<div class="setting-block">
<span class="setting-label">チームの数</span>
<div class="btn-row" id="teamCountRow">
<button class="opt-btn" data-count="2" type="button">2チーム</button>
<button class="opt-btn active" data-count="3" type="button">3チーム</button>
<button class="opt-btn" data-count="4" type="button">4チーム</button>
</div>
</div>
<div id="teamNameInputs"></div>
<button id="team-start-btn" type="button">対決スタート!</button>
<button id="team-cancel-btn" type="button">タイトルへもどる</button>
</div>
</section>
<section id="work-screen" hidden>
<header>
<span class="title" id="caseTitle">調査ファイル</span>
<span class="timer" id="timerDisplay">0:00</span>
<button class="icon-btn" id="glossaryBtn" type="button">📖 用語辞典</button>
</header>
<div id="main-area">
<div id="grid-panel">
<div id="remaining-count"></div>
<div id="grid-scroll">
<div id="numGrid"></div>
</div>
</div>
<div id="clue-panel">
<h2>🔍 手がかり</h2>
<ul id="clueList"></ul>
<button id="next-clue-btn" type="button">つぎの手がかり</button>
</div>
</div>
<div id="bottom-bar">
<button id="solve-btn" type="button" disabled>🔎 お宝発見!</button>
<div id="solveMsg"></div>
</div>
</section>
<section id="analysis-screen" hidden>
<header>
<span class="title">🔬 重ねて分析</span>
<button class="icon-btn" id="analysisBackBtn" type="button">← タイトルへ</button>
</header>
<div id="analysis-controls">
<div class="mode-tabs" id="analysisModeTabs">
<button class="tab-btn active" data-mode="multiple" type="button">倍数で分析</button>
<button class="tab-btn" data-mode="divisor" type="button">約数で分析</button>
</div>
<div class="num-pickers">
<label>数字A <input type="number" id="numA" min="1" max="100" value="4"></label>
<label>数字B <input type="number" id="numB" min="1" max="100" value="6"></label>
</div>
</div>
<div id="analysis-summary"></div>
<div id="analysis-grid-wrap">
<div id="analysisGrid"></div>
</div>
</section>
<div id="done-overlay">
<div id="done-card">
<h2>🎉 お宝発見!</h2>
<p id="doneText"></p>
<div id="done-send-row">
<input type="text" id="done-name-input" placeholder="なまえを入力してね">
<button id="done-send-btn" type="button">📤 記録を送る</button>
</div>
<div id="done-send-status"></div>
<button id="nextCaseBtn" type="button">つぎのお宝へ</button>
</div>
</div>
<div id="team-transition-overlay">
<div id="team-transition-card">
<h2 id="teamTransitionTitle"></h2>
<p id="teamTransitionSub">じゅんびができたら、下のボタンをおしてね</p>
<button id="team-transition-start" type="button">この チームで スタート</button>
</div>
</div>
<div id="team-leaderboard-overlay">
<div id="team-leaderboard-card">
<h2>🏆 けっか はっぴょう!</h2>
<p class="sub-note">手がかりの数が すくないほど 上い!(同じ数なら タイムが はやい方が上い)</p>
<ol id="teamLeaderboardList"></ol>
<button id="team-finish-btn" type="button">タイトルへもどる</button>
</div>
</div>
<div id="glossary-overlay">
<div id="glossary-card">
<h2>📖 用語辞典</h2>
<div id="termList"></div>
<button id="glossary-close" type="button">とじる</button>
</div>
</div>
</div>
<script>
(function(){
const titleScreen = document.getElementById('title-screen');
const workScreen = document.getElementById('work-screen');
const rangeRow = document.getElementById('rangeRow');
const unitRow = document.getElementById('unitRow');
const startBtn = document.getElementById('start-btn');
const caseTitle = document.getElementById('caseTitle');
const numGrid = document.getElementById('numGrid');
const remainingCountEl = document.getElementById('remaining-count');
const clueList = document.getElementById('clueList');
const nextClueBtn = document.getElementById('next-clue-btn');
const solveBtn = document.getElementById('solve-btn');
const solveMsg = document.getElementById('solveMsg');
const doneOverlay = document.getElementById('done-overlay');
const doneText = document.getElementById('doneText');
const nextCaseBtn = document.getElementById('nextCaseBtn');
const timerDisplay = document.getElementById('timerDisplay');
const doneNameInput = document.getElementById('done-name-input');
const doneSendBtn = document.getElementById('done-send-btn');
const doneSendStatus = document.getElementById('done-send-status');
const teamOpenBtn = document.getElementById('team-open-btn');
const teamSetupScreen = document.getElementById('team-setup-screen');
const teamCountRow = document.getElementById('teamCountRow');
const teamNameInputs = document.getElementById('teamNameInputs');
const teamStartBtn = document.getElementById('team-start-btn');
const teamCancelBtn = document.getElementById('team-cancel-btn');
const teamTransitionOverlay = document.getElementById('team-transition-overlay');
const teamTransitionTitle = document.getElementById('teamTransitionTitle');
const teamTransitionStart = document.getElementById('team-transition-start');
const teamLeaderboardOverlay = document.getElementById('team-leaderboard-overlay');
const teamLeaderboardList = document.getElementById('teamLeaderboardList');
const teamFinishBtn = document.getElementById('team-finish-btn');
const glossaryBtn = document.getElementById('glossaryBtn');
const glossaryOpenBtn = document.getElementById('glossary-open-btn');
const glossaryOverlay = document.getElementById('glossary-overlay');
const glossaryClose = document.getElementById('glossary-close');
const termList = document.getElementById('termList');
const TERMS = [
{ term: '偶数', def: '2でわりきれる整数のこと。', ex: '例:0, 2, 4, 6, 8…' },
{ term: '奇数', def: '2でわりきれない整数のこと。', ex: '例:1, 3, 5, 7, 9…' },
{ term: '倍数', def: 'ある数を1倍、2倍、3倍…してできる数のこと。', ex: '例:3の倍数 → 3, 6, 9, 12…' },
{ term: '約数', def: 'ある数をわりきることができる整数のこと。', ex: '例:12の約数 → 1, 2, 3, 4, 6, 12' },
{ term: '公倍数', def: '2つ以上の数に共通する倍数のこと。', ex: '例:4と6の公倍数 → 12, 24, 36…' },
{ term: '最小公倍数', def: '公倍数の中でいちばん小さい数のこと。', ex: '例:4と6の最小公倍数 → 12' },
{ term: '公約数', def: '2つ以上の数に共通する約数のこと。', ex: '例:12と18の公約数 → 1, 2, 3, 6' },
{ term: '最大公約数', def: '公約数の中でいちばん大きい数のこと。', ex: '例:12と18の最大公約数 → 6' },
];
termList.innerHTML = TERMS.map((t) =>
'<div class="term-item"><div class="term">' + t.term + '</div>' +
'<div class="def">' + t.def + '</div>' +
'<div class="ex">' + t.ex + '</div></div>'
).join('');
function openGlossary(){ glossaryOverlay.classList.add('show'); }
glossaryBtn.addEventListener('click', openGlossary);
glossaryOpenBtn.addEventListener('click', openGlossary);
glossaryClose.addEventListener('click', () => glossaryOverlay.classList.remove('show'));
// ---------------- 重ねて分析 ----------------
const analysisOpenBtn = document.getElementById('analysis-open-btn');
const analysisScreen = document.getElementById('analysis-screen');
const analysisBackBtn = document.getElementById('analysisBackBtn');
const analysisModeTabs = document.getElementById('analysisModeTabs');
const numAInput = document.getElementById('numA');
const numBInput = document.getElementById('numB');
const analysisSummary = document.getElementById('analysis-summary');
const analysisGrid = document.getElementById('analysisGrid');
let analysisMode = 'multiple';
const ANALYSIS_DEFAULTS = { multiple: [4, 6], divisor: [12, 18] };
function clamp(n, lo, hi){ return Math.max(lo, Math.min(hi, n)); }
function renderAnalysis(){
const a = clamp(Math.floor(Number(numAInput.value)) || 1, 1, 100);
const b = clamp(Math.floor(Number(numBInput.value)) || 1, 1, 100);
numAInput.value = a;
numBInput.value = b;
analysisGrid.innerHTML = '';
const commonList = [];
for(let n = 1; n <= 100; n++){
const cell = document.createElement('div');
cell.className = 'analysis-cell';
cell.textContent = n;
let isA, isB;
if(analysisMode === 'multiple'){
isA = n % a === 0;
isB = n % b === 0;
} else {
isA = a % n === 0;
isB = b % n === 0;
}
if(isA && isB){
cell.classList.add('common');
commonList.push(n);
} else if(isA){
cell.classList.add('markA');
} else if(isB){
cell.classList.add('markB');
}
analysisGrid.appendChild(cell);
}
if(analysisMode === 'multiple'){
const lcm = commonList.length ? Math.min.apply(null, commonList) : null;
const shown = commonList.slice(0, 8).join(', ') + (commonList.length > 8 ? ' …' : '');
analysisSummary.textContent = a + ' と ' + b + ' の公倍数(青×オレンジが重なった紫のマス): ' +
(commonList.length ? shown : 'なし') + (lcm !== null ? ' 最小公倍数 → ' + lcm : '');
} else {
const gcd = commonList.length ? Math.max.apply(null, commonList) : null;
analysisSummary.textContent = a + ' と ' + b + ' の公約数(紫のマス): ' +
(commonList.length ? commonList.join(', ') : 'なし') + (gcd !== null ? ' 最大公約数 → ' + gcd : '');
}
// 最小公倍数/最大公約数のマスを強調する
analysisGrid.querySelectorAll('.analysis-cell.common').forEach((cell) => {
const n = Number(cell.textContent);
const best = analysisMode === 'multiple' ? Math.min.apply(null, commonList) : Math.max.apply(null, commonList);
cell.classList.toggle('best', n === best);
});
}
analysisModeTabs.addEventListener('click', (e) => {
const btn = e.target.closest('.tab-btn');
if(!btn) return;
analysisMode = btn.dataset.mode;
analysisModeTabs.querySelectorAll('.tab-btn').forEach((b) => b.classList.toggle('active', b === btn));
const defaults = ANALYSIS_DEFAULTS[analysisMode];
numAInput.value = defaults[0];
numBInput.value = defaults[1];
renderAnalysis();
});
numAInput.addEventListener('input', renderAnalysis);
numBInput.addEventListener('input', renderAnalysis);
analysisOpenBtn.addEventListener('click', () => {
titleScreen.hidden = true;
analysisScreen.hidden = false;
renderAnalysis();
});
analysisBackBtn.addEventListener('click', () => {
analysisScreen.hidden = true;
titleScreen.hidden = false;
});
// ---------------- グループ対決 ----------------
let teamCount = 3;
let teamState = null; // { teams: [name,...], currentIndex: 0, results: [] }
function renderTeamNameInputs(){
teamNameInputs.innerHTML = '';
for(let i = 0; i < teamCount; i++){
const input = document.createElement('input');
input.type = 'text';
input.className = 'team-name-input';
input.value = String.fromCharCode(65 + i) + 'チーム';
input.maxLength = 12;
input.dataset.teamIndex = i;
teamNameInputs.appendChild(input);
}
}
teamCountRow.addEventListener('click', (e) => {
const btn = e.target.closest('.opt-btn');
if(!btn) return;
teamCount = Number(btn.dataset.count);
teamCountRow.querySelectorAll('.opt-btn').forEach((b) => b.classList.toggle('active', b === btn));
renderTeamNameInputs();
});
teamOpenBtn.addEventListener('click', () => {
titleScreen.hidden = true;
teamSetupScreen.hidden = false;
renderTeamNameInputs();
});
teamCancelBtn.addEventListener('click', () => {
teamSetupScreen.hidden = true;
titleScreen.hidden = false;
});
function showTeamTransition(){
const name = teamState.teams[teamState.currentIndex];
teamTransitionTitle.textContent = name + ' の番です!';
teamTransitionOverlay.classList.add('show');
}
teamStartBtn.addEventListener('click', () => {
const names = Array.from(teamNameInputs.querySelectorAll('.team-name-input')).map((el, i) => el.value.trim() || (String.fromCharCode(65 + i) + 'チーム'));
teamState = { teams: names, currentIndex: 0, results: [] };
teamSetupScreen.hidden = true;
showTeamTransition();
});
teamTransitionStart.addEventListener('click', () => {
teamTransitionOverlay.classList.remove('show');
workScreen.hidden = false;
startCase();
});
function showLeaderboard(){
const sorted = teamState.results.slice().sort((a, b) => {
if(a.clueCount !== b.clueCount) return a.clueCount - b.clueCount;
return a.elapsedMs - b.elapsedMs;
});
teamLeaderboardList.innerHTML = sorted.map((r) =>
'<li>' + r.team + ' — 手がかり ' + r.clueCount + ' 個 / タイム ' + formatTime(r.elapsedMs) + '</li>'
).join('');
teamLeaderboardOverlay.classList.add('show');
}
teamFinishBtn.addEventListener('click', () => {
teamState = null;
teamLeaderboardOverlay.classList.remove('show');
titleScreen.hidden = false;
});
let range = 50;
let useUnits = { evenodd: true, multiple: true, divisor: true };
rangeRow.addEventListener('click', (e) => {
const btn = e.target.closest('.opt-btn');
if(!btn) return;
range = Number(btn.dataset.range);
rangeRow.querySelectorAll('.opt-btn').forEach((b) => b.classList.toggle('active', b === btn));
});
unitRow.addEventListener('click', (e) => {
const btn = e.target.closest('.opt-btn');
if(!btn) return;
const key = btn.dataset.unit;
useUnits[key] = !useUnits[key];
btn.classList.toggle('active', useUnits[key]);
});
function shuffle(arr){
const a = arr.slice();
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 pickComparisonClue(candidates, secret){
const sorted = candidates.slice().sort((a, b) => a - b);
const secretIdx = sorted.indexOf(secret);
let pivotIdx = Math.floor(sorted.length / 2);
if(pivotIdx === secretIdx){
pivotIdx = (pivotIdx + 1 < sorted.length) ? pivotIdx + 1 : pivotIdx - 1;
}
const pivot = sorted[pivotIdx];
if(secret > pivot){
return { text: pivot + ' より 大きい数です', filter: (n) => n > pivot };
}
return { text: pivot + ' より 小さい数です', filter: (n) => n < pivot };
}
// 手がかりの種類ごとの生成関数。null を返したら「今回は使えない」の意味。
// candidates を渡すもの(between)以外は secret / rangeN だけで決まる。
function makeClueGenerators(secret, rangeN){
return {
onesDigit: () => {
const ones = secret % 10;
return { text: '一の位は ' + ones + ' です', filter: (n) => n % 10 === ones };
},
tensDigit: () => {
if(secret < 10) return null;
const tens = Math.floor(secret / 10) % 10;
return { text: '十の位は ' + tens + ' です', filter: (n) => n >= 10 && Math.floor(n / 10) % 10 === tens };
},
between: (candidates) => {
const sorted = candidates.slice().sort((a, b) => a - b);
if(sorted.length < 5) return null;
const idx = sorted.indexOf(secret);
const half = Math.max(1, Math.floor(sorted.length * (0.2 + Math.random() * 0.25)));
const loIdx = Math.max(0, idx - half);
const hiIdx = Math.min(sorted.length - 1, idx + half);
if(loIdx === 0 && hiIdx === sorted.length - 1) return null;
const lo = sorted[loIdx], hi = sorted[hiIdx];
return { text: lo + ' 以上 ' + hi + ' 以下です', filter: (n) => n >= lo && n <= hi };
},
evenOdd: () => {
const isEven = secret % 2 === 0;
return { text: isEven ? '偶数です' : '奇数です', filter: (n) => (n % 2 === 0) === isEven };
},
multiple: () => {
const niceDivisors = [2,3,4,5,6,7,8,9,10].filter((d) => d < secret && secret % d === 0);
if(niceDivisors.length === 0) return null;
const d = niceDivisors[Math.floor(Math.random() * niceDivisors.length)];
return { text: d + ' の倍数です', filter: (n) => n % d === 0 };
},
divisorOf: () => {
const multiplier = 2 + Math.floor(Math.random() * 3);
const M = secret * multiplier;
if(M > rangeN * 4) return null;
return { text: M + ' の約数です', filter: (n) => M % n === 0 };
},
commonMultiple: () => {
const divisors = [2,3,4,5,6,7,8,9,10].filter((d) => d < secret && secret % d === 0);
if(divisors.length < 2) return null;
const shuffled = shuffle(divisors);
const a = shuffled[0], b = shuffled[1];
return { text: a + ' と ' + b + ' の公倍数です', filter: (n) => n % a === 0 && n % b === 0 };
},
commonDivisor: () => {
const m1 = 2 + Math.floor(Math.random() * 3);
let m2 = 2 + Math.floor(Math.random() * 3);
if(m2 === m1) m2 = m1 + 1;
const M1 = secret * m1, M2 = secret * m2;
if(M1 > rangeN * 4 || M2 > rangeN * 4) return null;
return { text: M1 + ' と ' + M2 + ' の公約数です', filter: (n) => M1 % n === 0 && M2 % n === 0 };
},
};
}
function generateClues(secret, rangeN, units){
let candidates = Array.from({ length: rangeN }, (_, i) => i + 1);
const clues = [];
const gens = makeClueGenerators(secret, rangeN);
function addClue(clue){
clues.push(clue);
candidates = candidates.filter(clue.filter);
}
// 「けたの数」は役に立つときだけ、必ず最初に出す
const digitLen = String(secret).length;
const digitFilter = (n) => String(n).length === digitLen;
if(candidates.filter(digitFilter).length < candidates.length){
addClue({ text: digitLen + ' けたの数です', filter: digitFilter });
}
const basicKeys = ['onesDigit', 'tensDigit', 'between'];
const unitKeys = [];
if(units.evenodd) unitKeys.push('evenOdd');
if(units.multiple) unitKeys.push('multiple', 'commonMultiple');
if(units.divisor) unitKeys.push('divisorOf', 'commonDivisor');
const usedKeys = new Set();
let guard = 0;
while(candidates.length > 1 && guard < 30){
guard++;
const pool = shuffle(basicKeys.concat(unitKeys).filter((k) => !usedKeys.has(k)));
let applied = false;
for(const key of pool){
const clue = gens[key](candidates);
if(!clue) continue;
const filtered = candidates.filter(clue.filter);
if(filtered.length > 0 && filtered.length < candidates.length){
addClue(clue);
usedKeys.add(key);
applied = true;
break;
}
}
if(!applied){
// 上のどれも使えない/しぼり込めないときは、必ず解決できる比較の手がかりで確実に進める
addClue(pickComparisonClue(candidates, secret));
}
}
return clues;
}
function safeGet(key){ try{ return localStorage.getItem(key); } catch(e){ return null; } }
function safeSet(key, val){ try{ localStorage.setItem(key, val); } catch(e){} }
const NAME_KEY = 'detective-number-mystery-name';
doneNameInput.value = safeGet(NAME_KEY) || '';
let secret = null;
let clues = [];
let clueIndex = 0;
let eliminated = new Set();
let solved = false;
let caseStartTime = null;
let timerId = null;
let elapsedMs = 0;
function formatTime(ms){
const totalSec = Math.floor(ms / 1000);
const m = Math.floor(totalSec / 60);
const s = totalSec % 60;
return m + ':' + String(s).padStart(2, '0');
}
function startTimer(){
caseStartTime = Date.now();
timerDisplay.textContent = '0:00';
clearInterval(timerId);
timerId = setInterval(() => {
timerDisplay.textContent = formatTime(Date.now() - caseStartTime);
}, 500);
}
function stopTimer(){
clearInterval(timerId);
elapsedMs = Date.now() - caseStartTime;
return elapsedMs;
}
function buildGrid(){
numGrid.innerHTML = '';
for(let n = 1; n <= range; n++){
const cell = document.createElement('div');
cell.className = 'num-cell';
cell.textContent = n;
cell.dataset.n = n;
cell.addEventListener('click', () => toggleCell(n));
numGrid.appendChild(cell);
}
}
function remainingCandidates(){
const all = [];
for(let n = 1; n <= range; n++) if(!eliminated.has(n)) all.push(n);
return all;
}
function refreshGrid(){
const remaining = remainingCandidates();
numGrid.querySelectorAll('.num-cell').forEach((cell) => {
const n = Number(cell.dataset.n);
cell.classList.toggle('eliminated', eliminated.has(n));
cell.classList.toggle('lone-candidate', remaining.length === 1 && !eliminated.has(n));
});
remainingCountEl.textContent = '候補:のこり ' + remaining.length + ' こ';
solveBtn.disabled = remaining.length !== 1 || solved;
}
function toggleCell(n){
if(solved) return;
if(eliminated.has(n)) eliminated.delete(n);
else eliminated.add(n);
refreshGrid();
}
function renderClues(){
clueList.innerHTML = '';
clues.slice(0, clueIndex).forEach((c, i) => {
const li = document.createElement('li');
li.textContent = (i + 1) + '. ' + c.text;
clueList.appendChild(li);
});
nextClueBtn.disabled = clueIndex >= clues.length;
nextClueBtn.textContent = clueIndex >= clues.length ? 'ぜんぶの手がかりが出ました' : 'つぎの手がかり(のこり ' + (clues.length - clueIndex) + ')';
}
nextClueBtn.addEventListener('click', () => {
if(clueIndex < clues.length){
clueIndex++;
renderClues();
}
});
solveBtn.addEventListener('click', () => {
const remaining = remainingCandidates();
if(remaining.length !== 1) return;
const guess = remaining[0];
if(guess === secret){
solved = true;
const ms = stopTimer();
solveMsg.textContent = '';
if(teamState){
teamState.results.push({
team: teamState.teams[teamState.currentIndex],
clueCount: clueIndex,
elapsedMs: ms,
});
teamState.currentIndex++;
workScreen.hidden = true;
if(teamState.currentIndex < teamState.teams.length){
showTeamTransition();
} else {
showLeaderboard();
}
return;
}
doneText.textContent = 'お宝の数字は ' + secret + ' でした! ' + clueIndex + ' 個の手がかりで、' + formatTime(ms) + ' で見つけられたね。';
doneOverlay.classList.add('show');
doneSendStatus.textContent = '';
doneSendBtn.disabled = false;
} else {
solveMsg.textContent = 'ちがうようです…消したマスをもう一度見直してみよう。';
solveMsg.style.color = '#C0392B';
}
});
doneSendBtn.addEventListener('click', () => {
const name = doneNameInput.value.trim();
if(!name){
doneSendStatus.textContent = 'なまえを入力してね';
doneSendStatus.style.color = '#C0392B';
doneNameInput.focus();
return;
}
safeSet(NAME_KEY, name);
if(typeof google === 'undefined' || !google.script || !google.script.run){
doneSendStatus.textContent = 'この機能はGoogleスプレッドシート連携版として配信された環境でのみ使えます。';
doneSendStatus.style.color = '#C0392B';
return;
}
doneSendBtn.disabled = true;
doneSendStatus.textContent = '送信中…';
doneSendStatus.style.color = '#475569';
google.script.run
.withSuccessHandler(function(){
doneSendStatus.textContent = '記録を送りました!';
doneSendStatus.style.color = '#4F8A4B';
})
.withFailureHandler(function(){
doneSendStatus.textContent = '送信に失敗しました。もう一度試してね';
doneSendStatus.style.color = '#C0392B';
doneSendBtn.disabled = false;
})
.sendDetectiveResult({
name: name,
range: range,
secret: secret,
clueCount: clueIndex,
elapsedMs: elapsedMs,
elapsedText: formatTime(elapsedMs),
units: useUnits,
});
});
nextCaseBtn.addEventListener('click', () => {
doneOverlay.classList.remove('show');
doneSendBtn.disabled = false;
startCase();
});
function beginGameWithClues(newSecret, newClues){
solved = false;
eliminated = new Set();
clueIndex = 0;
solveMsg.textContent = '';
secret = newSecret;
clues = newClues;
caseTitle.textContent = '調査ファイル(1〜' + range + 'の中に お宝の数字が1つ)';
buildGrid();
refreshGrid();
renderClues();
startTimer();
}
function startCase(){
const newSecret = 1 + Math.floor(Math.random() * range);
const newClues = generateClues(newSecret, range, useUnits);
beginGameWithClues(newSecret, newClues);
}
startBtn.addEventListener('click', () => {
titleScreen.hidden = true;
workScreen.hidden = false;
startCase();
});
// ---------------- 挑戦状づくり ----------------
const challengeOpenBtn = document.getElementById('challenge-open-btn');
const challengeReceiveOpenBtn = document.getElementById('challenge-receive-open-btn');
const challengeCreateScreen = document.getElementById('challenge-create-screen');
const challengeReceiveScreen = document.getElementById('challenge-receive-screen');
const challengeRangeRow = document.getElementById('challengeRangeRow');
const challengeSecretInput = document.getElementById('challengeSecretInput');
const challengeClueSection = document.getElementById('challengeClueSection');
const challengeClueChecks = document.getElementById('challengeClueChecks');
const challengeStatus = document.getElementById('challengeStatus');
const challengeMakeBtn = document.getElementById('challengeMakeBtn');
const challengeCreateBackBtn = document.getElementById('challengeCreateBackBtn');
const challengeResult = document.getElementById('challengeResult');
const challengeCode = document.getElementById('challengeCode');
const challengeCopyBtn = document.getElementById('challengeCopyBtn');
const challengeReceiveInput = document.getElementById('challengeReceiveInput');
const challengeReceiveBtn = document.getElementById('challengeReceiveBtn');
const challengeReceiveStatus = document.getElementById('challengeReceiveStatus');
const challengeReceiveBackBtn = document.getElementById('challengeReceiveBackBtn');
let challengeRange = 50;
let challengeSelectedDefs = [];
// 手がかりの「型」から実際の text / filter を組み立てる。
// グループ対決などの自動生成(makeClueGenerators)とはちがい、
// ここではパラメータ込みで完全に決まった手がかりを1つだけ返す。
function buildClueFromDef(secret, def){
if(def.type === 'digitCount'){
const len = String(secret).length;
return { text: len + ' けたの数です', filter: (n) => String(n).length === len };
}
if(def.type === 'onesDigit'){
const ones = secret % 10;
return { text: '一の位は ' + ones + ' です', filter: (n) => n % 10 === ones };
}
if(def.type === 'tensDigit'){
const tens = Math.floor(secret / 10) % 10;
return { text: '十の位は ' + tens + ' です', filter: (n) => n >= 10 && Math.floor(n / 10) % 10 === tens };
}
if(def.type === 'evenOdd'){
const isEven = secret % 2 === 0;
return { text: isEven ? '偶数です' : '奇数です', filter: (n) => (n % 2 === 0) === isEven };
}
if(def.type === 'multiple' && def.params && typeof def.params.d === 'number'){
const d = def.params.d;
return { text: d + ' の倍数です', filter: (n) => n % d === 0 };
}
if(def.type === 'commonMultiple' && def.params && typeof def.params.a === 'number' && typeof def.params.b === 'number'){
const a = def.params.a, b = def.params.b;
return { text: a + ' と ' + b + ' の公倍数です', filter: (n) => n % a === 0 && n % b === 0 };
}
if(def.type === 'divisorOf' && def.params && typeof def.params.mult === 'number'){
const M = secret * def.params.mult;
return { text: M + ' の約数です', filter: (n) => M % n === 0 };
}
if(def.type === 'commonDivisor' && def.params && typeof def.params.m1 === 'number' && typeof def.params.m2 === 'number'){
const M1 = secret * def.params.m1, M2 = secret * def.params.m2;
return { text: M1 + ' と ' + M2 + ' の公約数です', filter: (n) => M1 % n === 0 && M2 % n === 0 };
}
return null;
}
function computeAvailableClueDefs(secret, rangeN){
const list = [];
list.push({ type: 'onesDigit', params: {} });
if(secret >= 10) list.push({ type: 'tensDigit', params: {} });
list.push({ type: 'evenOdd', params: {} });
const niceDivisors = [2,3,4,5,6,7,8,9,10].filter((d) => d < secret && secret % d === 0);
if(niceDivisors.length > 0){
list.push({ type: 'multiple', params: { d: niceDivisors[0] } });
}
if(niceDivisors.length >= 2){
list.push({ type: 'commonMultiple', params: { a: niceDivisors[0], b: niceDivisors[1] } });
}
if(secret * 2 <= rangeN * 4){
list.push({ type: 'divisorOf', params: { mult: 2 } });
}
if(secret * 2 <= rangeN * 4 && secret * 3 <= rangeN * 4){
list.push({ type: 'commonDivisor', params: { m1: 2, m2: 3 } });
}
return list;
}
function renderChallengeClueChecks(){
const secret = Number(challengeSecretInput.value);
if(!secret || secret < 1 || secret > challengeRange || !Number.isInteger(secret)){
challengeClueSection.hidden = true;
challengeMakeBtn.disabled = true;
challengeResult.hidden = true;
return;
}
challengeClueSection.hidden = false;
const availableDefs = computeAvailableClueDefs(secret, challengeRange);
challengeClueChecks.innerHTML = '';
availableDefs.forEach((def, i) => {
const clue = buildClueFromDef(secret, def);
const row = document.createElement('label');
row.className = 'clue-check-row';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.dataset.defIndex = i;
const span = document.createElement('span');
span.textContent = clue.text;
row.appendChild(cb);
row.appendChild(span);
challengeClueChecks.appendChild(row);
});
challengeClueChecks.dataset.available = JSON.stringify(availableDefs);
updateChallengePreview();
}
function updateChallengePreview(){
const secret = Number(challengeSecretInput.value);
const availableDefs = JSON.parse(challengeClueChecks.dataset.available || '[]');
const checkedIdx = Array.from(challengeClueChecks.querySelectorAll('input:checked')).map((cb) => Number(cb.dataset.defIndex));
const selectedDefs = [{ type: 'digitCount', params: {} }].concat(checkedIdx.map((i) => availableDefs[i]));
let candidates = Array.from({ length: challengeRange }, (_, i) => i + 1);
selectedDefs.forEach((def) => {
const clue = buildClueFromDef(secret, def);
candidates = candidates.filter(clue.filter);
});
if(candidates.length === 1){
challengeStatus.textContent = '✅ これで1つにしぼれます!「合言葉をつくる」がおせるよ。';
challengeStatus.style.color = '#4F8A4B';
challengeMakeBtn.disabled = false;
} else {
challengeStatus.textContent = '候補が ' + candidates.length + ' こ のこっています。手がかりを増やしてみよう。';
challengeStatus.style.color = '#C0392B';
challengeMakeBtn.disabled = true;
}
challengeSelectedDefs = selectedDefs;
challengeResult.hidden = true;
}
challengeRangeRow.addEventListener('click', (e) => {
const btn = e.target.closest('.opt-btn');
if(!btn) return;
challengeRange = Number(btn.dataset.range);
challengeRangeRow.querySelectorAll('.opt-btn').forEach((b) => b.classList.toggle('active', b === btn));
challengeSecretInput.max = challengeRange;
renderChallengeClueChecks();
});
challengeSecretInput.addEventListener('input', renderChallengeClueChecks);
challengeClueChecks.addEventListener('change', updateChallengePreview);
function encodeChallenge(secret, rangeVal, defs){
const payload = { v: 1, r: rangeVal, s: secret, c: defs };
try {
return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
} catch(e){
return null;
}
}
function decodeChallenge(code){
try {
const json = decodeURIComponent(escape(atob(code.trim())));
const payload = JSON.parse(json);
if(!payload || typeof payload.s !== 'number' || typeof payload.r !== 'number' || !Array.isArray(payload.c)) return null;
if(payload.s < 1 || payload.s > payload.r) return null;
return payload;
} catch(e){
return null;
}
}
challengeMakeBtn.addEventListener('click', () => {
const secret = Number(challengeSecretInput.value);
const code = encodeChallenge(secret, challengeRange, challengeSelectedDefs);
if(!code){
challengeStatus.textContent = '合言葉づくりに失敗しました。もう一度試してね。';
challengeStatus.style.color = '#C0392B';
return;
}
challengeCode.textContent = code;
challengeResult.hidden = false;
});
challengeCopyBtn.addEventListener('click', () => {
const text = challengeCode.textContent;
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(text).then(() => {
challengeCopyBtn.textContent = '✅ コピーしました';
setTimeout(() => { challengeCopyBtn.textContent = '📋 コピーする'; }, 1500);
}).catch(() => {});
}
});
challengeOpenBtn.addEventListener('click', () => {
titleScreen.hidden = true;
challengeCreateScreen.hidden = false;
challengeSecretInput.value = '';
challengeClueSection.hidden = true;
challengeResult.hidden = true;
challengeMakeBtn.disabled = true;
});
challengeCreateBackBtn.addEventListener('click', () => {
challengeCreateScreen.hidden = true;
titleScreen.hidden = false;
});
challengeReceiveOpenBtn.addEventListener('click', () => {
titleScreen.hidden = true;
challengeReceiveScreen.hidden = false;
challengeReceiveInput.value = '';
challengeReceiveStatus.textContent = '';
});
challengeReceiveBackBtn.addEventListener('click', () => {
challengeReceiveScreen.hidden = true;
titleScreen.hidden = false;
});
challengeReceiveBtn.addEventListener('click', () => {
const payload = decodeChallenge(challengeReceiveInput.value);
if(!payload){
challengeReceiveStatus.textContent = '合言葉が正しくないようです。もう一度たしかめてね。';
challengeReceiveStatus.style.color = '#C0392B';
return;
}
const reconstructed = [];
for(const def of payload.c){
const clue = buildClueFromDef(payload.s, def);
if(!clue){
challengeReceiveStatus.textContent = '合言葉が正しくないようです。もう一度たしかめてね。';
challengeReceiveStatus.style.color = '#C0392B';
return;
}
reconstructed.push(clue);
}
let candidates = Array.from({ length: payload.r }, (_, i) => i + 1);
reconstructed.forEach((clue) => { candidates = candidates.filter(clue.filter); });
if(reconstructed.length === 0 || candidates.length !== 1 || candidates[0] !== payload.s){
challengeReceiveStatus.textContent = 'この合言葉では正しく調査できません。作りなおしてもらってね。';
challengeReceiveStatus.style.color = '#C0392B';
return;
}
range = payload.r;
challengeReceiveScreen.hidden = true;
workScreen.hidden = false;
beginGameWithClues(payload.s, reconstructed);
});
})();
</script>
</body>
</html>
/**
* 「お宝発見!すうじ調査団」 - みんなの調査記録をスプレッドシートに送る機能
*
* 使い方(デプロイ手順)は README.txt を参照してください。
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('お宝発見!すうじ調査団')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getOrCreateSpreadsheet_() {
const props = PropertiesService.getScriptProperties();
const savedId = props.getProperty('SPREADSHEET_ID');
if (savedId) {
try {
return SpreadsheetApp.openById(savedId);
} catch (err) {
// 保存されていたIDが無効な場合は作り直す
}
}
const ss = SpreadsheetApp.create('お宝発見!すうじ調査団 - みんなの調査記録');
const sheet = ss.getSheets()[0];
sheet.appendRow(['送信日時', 'なまえ', 'はんい', 'お宝の数字', '使った手がかりの数', 'かかった時間', '偶数奇数', '倍数', '約数']);
sheet.setFrozenRows(1);
props.setProperty('SPREADSHEET_ID', ss.getId());
return ss;
}
/**
* フロント側から google.script.run 経由で呼ばれる。
* data = {
* name: string, range: number, secret: number, clueCount: number,
* elapsedMs: number, elapsedText: string,
* units: { evenodd: boolean, multiple: boolean, divisor: boolean }
* }
*/
function sendDetectiveResult(data) {
if (!data || !data.name) {
throw new Error('なまえがありません');
}
const ss = getOrCreateSpreadsheet_();
const sheet = ss.getSheets()[0];
const units = data.units || {};
sheet.appendRow([
new Date(),
data.name.toString().slice(0, 40),
'1〜' + data.range,
data.secret,
data.clueCount,
data.elapsedText || '',
units.evenodd ? '○' : '-',
units.multiple ? '○' : '-',
units.divisor ? '○' : '-',
]);
return { ok: true };
}
「お宝発見!すうじ調査団」 スプレッドシート送信版 - デプロイ手順
====================================================
このフォルダには3つのファイルがあります。
・index.html … 児童が触る画面(オフライン版に「なまえ」欄と「送る」ボタンを追加したもの)
・Code.gs … スプレッドシートにデータを送るためのバックエンド処理
・README.txt … このファイル
【1】Google Apps Script プロジェクトを作る
1. Google ドライブを開く(学校アカウントでログイン)
2. 「新規」→「その他」→「Google Apps Script」
(見当たらない場合は https://script.google.com/ から「新しいプロジェクト」)
3. プロジェクト名を「お宝発見!すうじ調査団」などにする
【2】コードを貼り付ける
1. 左側のファイル一覧にある「Code.gs」を開き、中身をすべて削除
2. 渡した Code.gs の内容をすべて貼り付けて保存(Ctrl+S)
3. 左側の「+」→「HTML」を選び、ファイル名を必ず「index」にする(拡張子は自動でつきます)
4. 渡した index.html の内容をすべて貼り付けて保存
【3】ウェブアプリとして公開する
1. 右上の「デプロイ」→「新しいデプロイ」
2. 種類の選択(歯車マーク)で「ウェブアプリ」を選ぶ
3. 「次のユーザーとして実行」→ 自分(先生のアカウント)
4. 「アクセスできるユーザー」→ 学校のドメイン内の全員
(例:kita9.ed.jp 内の全員、など。学校のGoogle Workspace設定に合わせてください)
5. 「デプロイ」をクリックし、表示されたウェブアプリのURLをコピーする
6. 初回のみ、権限の承認画面が出るので許可する
【4】児童に配布する
・コピーしたURLをブックマークやGoogle Classroom等で共有すれば、
Chromebookのブラウザからそのまま使えます。
【5】スプレッドシートの確認
・誰かが初めて「送る」を押したときに、自動で
「お宝発見!すうじ調査団 - みんなのふりかえり」という
スプレッドシートがドライブに作成されます。
・以降の送信は、そのシートに1人1行ずつ追加されていきます。
・共有設定(誰が見られるか)は、作成後にドライブから
先生が調整してください(デフォルトは先生のみ閲覧可)。
【困ったときは】
・貼り付けたコードを直接書き換えても大丈夫です。次に相談するときは
「Code.gsの◯◯を直したい」のように伝えてもらえれば調整できます。