はじめに
中高生向けに、キュウリ出荷シミュレーションゲームをHTMLで作ってみました。

「今日の収穫、どこに出荷したら一番もうかる?」
そんな農家の意思決定を、ブラウザで遊べるミニゲームにしてみます。
このゲームは、天気・収穫量・品質・曲がり具合・需要*(SNSの話題)が毎日ランダムに変わり、あなたは出荷先を選びます。さらに、**“AIっぽい計算”**でおすすめを出すモデルも登場します。
できること(このゲームで学べること)
- ランダム(乱数)で「今日の状況」を作る
- 条件分岐(switch / if)で「出荷先ごとの特徴」を表現する
- 数式で「売れ残り」「単価」「利益」を計算する
- 複数の選択肢を比べて、最大のもの(AIのおすすめ)を選ぶ
- 「機械学習の考え方(特徴量→予測→評価)」の入口を体験する
遊び方
- 下のコードを index.html として保存
- ダブルクリックしてブラウザで開く(Chrome / Edge 推奨)
- 「新しい一日をはじめる」 を押して状況を生成
- 出荷先カードをクリックして利益を比較!
ルールのイメージ(ざっくり)
- スーパー:A品が多いと有利、曲がりは少しならOK
- 大都市の市場:高く売れるが、曲がりに厳しい&輸送コスト高
- 直売所:需要が高いと爆売れ、低いと売れ残り
- 工場:全部買い取ってくれるが単価は安め(ロスほぼなし)
“AIきゅうりモデル”って何?
本物の機械学習ライブラリは使っていません。
でもやっていることは「それっぽい」です:
- 入力(特徴量):天気・収穫量・品質・曲がり率・需要
- 出力(予測):各出荷先の利益を計算
- 評価:利益が最大の出荷先=AIおすすめ
本物の機械学習だと、この「利益の出やすさ(スコア)」を過去データから学習します。
今回は授業で説明しやすいように、ルール(計算式)で再現しています。
完成コード(コピペで動きます)
メモ帳を開いて以下のコードをコピペして、拡張子を.htmlにしてください。特別なライブラリーは要りません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>キュウリ出荷戦略ゲーム</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: linear-gradient(135deg, #e5ffe8, #f5fff8);
color: #234;
}
header {
background: #2e8b57;
color: #fff;
padding: 16px 20px;
text-align: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
header h1 { margin: 0; font-size: 1.8rem; }
header p { margin: 6px 0 0; font-size: 0.95rem; opacity: 0.9; }
main { max-width: 1100px; margin: 20px auto 40px; padding: 0 12px; }
.top-bar {
display: flex; flex-wrap: wrap; gap: 10px;
align-items: center; justify-content: space-between;
margin-bottom: 14px;
}
.top-bar button {
border: none; border-radius: 999px; padding: 8px 18px;
font-size: 0.95rem; cursor: pointer;
background: #2e8b57; color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
transition: transform 0.05s ease, box-shadow 0.05s ease, background 0.2s ease;
}
.top-bar button:hover { transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0,0,0,0.2); background: #36a067; }
.top-bar button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(0,0,0,0.25); }
.day-label { font-weight: 600; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 6px; }
.day-label span { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #fff; border: 1px solid #b8e0c5; }
.columns { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.1fr); gap: 18px; }
@media (max-width: 780px) { .columns { grid-template-columns: minmax(0, 1fr); } }
.card {
background: #ffffffd9;
border-radius: 16px;
padding: 14px 16px 16px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
border: 1px solid #cce9d4;
backdrop-filter: blur(4px);
}
.card h2 { margin: 0 0 8px; font-size: 1.15rem; display: flex; align-items: center; gap: 6px; }
.badge {
display: inline-flex; align-items: center; justify-content: center;
padding: 3px 8px; border-radius: 999px;
font-size: 0.7rem; font-weight: 600;
background: #e5f7eb; color: #2e8b57;
border: 1px solid #b8e0c5;
}
.scenario-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(140px,1fr)); gap: 8px; margin-top: 8px; font-size: 0.9rem; }
.scenario-item { background: #f5fff7; border-radius: 12px; padding: 8px 9px; border: 1px dashed #b8e0c5; }
.scenario-label { font-size: 0.8rem; color: #567; margin-bottom: 3px; }
.scenario-value { font-weight: 600; }
.scenario-hint { margin-top: 8px; font-size: 0.8rem; color: #566; line-height: 1.5; }
.hint-highlight { font-weight: 600; color: #2e8b57; }
.plan-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 4px; }
@media (max-width: 650px) { .plan-list { grid-template-columns: minmax(0,1fr); } }
.plan-card {
border-radius: 14px; padding: 10px 10px 11px;
background: #f7fff9; border: 1px solid #cfe8d6;
cursor: pointer; display: flex; flex-direction: column; gap: 4px;
transition: transform 0.05s ease, box-shadow 0.05s ease, border-color 0.1s ease, background 0.2s ease;
position: relative; overflow: hidden;
}
.plan-card::before { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at top, rgba(255,255,255,0.7), transparent 55%); opacity: 0; transition: opacity 0.15s ease; pointer-events: none; }
.plan-card:hover::before { opacity: 1; }
.plan-card:hover { transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0,0,0,0.12); border-color: #60bf7a; background: #f0fff4; }
.plan-card:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(0,0,0,0.16); }
.plan-title { font-weight: 700; font-size: 0.95rem; display: flex; align-items: center; gap: 6px; }
.plan-title span.icon { font-size: 1.1rem; }
.plan-desc { font-size: 0.8rem; line-height: 1.4; color: #556; }
.plan-meta { margin-top: 2px; font-size: 0.75rem; color: #678; display: flex; flex-wrap: wrap; gap: 6px; }
.pill { padding: 2px 7px; border-radius: 999px; background: #e8f6ec; border: 1px solid #c2e0cc; }
.result-card { margin-top: 8px; padding: 10px 12px; border-radius: 12px; background: #f4fff6; border: 1px solid #cce9d4; font-size: 0.9rem; line-height: 1.6; }
.result-row { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; margin-bottom: 3px; }
.result-label { font-weight: 600; }
.result-highlight { font-weight: 700; color: #1f8a55; }
.compare { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #b8e0c5; font-size: 0.85rem; }
.compare span.win { color: #1f8a55; font-weight: 700; }
.compare span.lose { color: #c0392b; font-weight: 700; }
.compare span.draw { color: #8e44ad; font-weight: 700; }
.ai-hint { margin-top: 6px; font-size: 0.8rem; color: #567; }
.ai-hint code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.78rem; background: #eaf6ee; padding: 1px 4px; border-radius: 4px; }
.ml-notes { margin-top: 18px; font-size: 0.85rem; line-height: 1.6; }
.ml-notes details { margin-top: 6px; padding: 8px 10px; border-radius: 10px; background: #f3fff7; border: 1px dashed #c0e0cc; }
.ml-notes summary { cursor: pointer; font-weight: 600; list-style: none; }
.ml-notes summary::marker, .ml-notes summary::-webkit-details-marker { display: none; }
.ml-notes summary span { margin-right: 4px; }
.ml-notes ul { margin: 6px 0 0 1.1em; padding: 0; }
.ml-notes li { margin-bottom: 2px; }
.tiny-caption { margin-top: 6px; font-size: 0.75rem; color: #6b7; }
.empty-message { font-size: 0.85rem; color: #566; margin-top: 4px; }
</style>
</head>
<body onload="init()">
<header>
<h1>🥒 キュウリ出荷戦略ゲーム</h1>
<p>あなたはキュウリ農家! 今日の状況を見て、どこに出荷すると一番儲かるかな?</p>
</header>
<main>
<div class="top-bar">
<div class="day-label">
今日のシミュレーション
<span id="dayText">Day 0</span>
</div>
<div>
<button id="newDayBtn" onclick="newDay()">🔄 新しい一日をはじめる</button>
</div>
</div>
<div class="columns">
<section class="card">
<h2>🌱 今日の畑のようす <span class="badge">入力なしでランダム生成</span></h2>
<p style="margin: 4px 0 6px; font-size: 0.85rem;">
ボタンを押すと、天気・収穫量・キュウリの曲がり具合・需要をランダムに決めます。
</p>
<div id="scenarioArea" class="scenario-grid"></div>
<div id="scenarioHint" class="scenario-hint">
🔍 「新しい一日をはじめる」を押して、今日の条件を決めてみよう。
</div>
</section>
<section class="card">
<h2>🚚 出荷先をえらぶ <span class="badge">AI きゅうりモデル も参戦</span></h2>
<p style="margin: 4px 0 8px; font-size: 0.85rem;">
下のどれかをクリックして、<span class="hint-highlight">全部のキュウリをそこに出荷</span>してみよう。
そのあとで、AIがこっそり計算したベスト戦略も表示されます。
</p>
<div class="plan-list" id="planList"></div>
<div id="resultArea" class="result-card" style="display:none;"></div>
<div id="emptyResult" class="empty-message">
※ まずは「新しい一日をはじめる」を押して状況を決めてから、出荷先を選んでみてね。
</div>
</section>
</div>
<section class="card ml-notes">
<h2>🤖 AIきゅうりモデルって何してるの?</h2>
<p>
このゲームでは、本物の機械学習ライブラリは使っていませんが、
「<span class="hint-highlight">機械学習モデルが考えそうなこと</span>」をマネしたルールでAIが出荷先を決めています。
</p>
<details>
<summary><span>▼</span>AIきゅうりモデルの「考えかた」(機械学習っぽいイメージ)</summary>
<ul>
<li>入力(特徴量)として、<strong>天気・収穫量・品質・曲がり率・需要レベル</strong>を使う。</li>
<li>それぞれの出荷先(スーパー/市場/直売所/工場)ごとに、利益が出そうかをスコア化する。</li>
<li>そのスコアが一番大きくなる出荷先を、<strong>「AIのおすすめ」</strong>として選ぶ。</li>
<li>本物の機械学習では、この<strong>「スコアの付け方」</strong>を
<span class="hint-highlight">過去のデータから自動で学習</span>する。
</li>
<li>このゲームでは、説明しやすいように、あらかじめ決めたルール(if文 & 計算)で真似しています。</li>
</ul>
<p class="tiny-caption">
※ 授業などで使う場合は、「入力(特徴量)→ 出力(予測)→ 利益を比べて評価」という流れを説明すると、
教師あり学習の導入にも使えます。
</p>
</details>
</section>
</main>
<script>
// ====== データ定義 ======
var state = {
day: 0, // ★Day表示がズレないように0から開始
scenario: null,
aiChoice: null
};
var plans = {
supermarket: {
key: "supermarket",
icon: "🏪",
name: "近所のスーパーに出荷",
desc: "A品が多いと高く買ってくれる。曲がりキュウリは少しは混ざってもOK。",
meta: ["価格:中〜高", "距離:近い", "残ったら少しロス"]
},
market: {
key: "market",
icon: "🏙️",
name: "大都市の市場に出荷",
desc: "品質さえ良ければ高く売れるが、形が悪いとハネられやすい。輸送コストも高め。",
meta: ["価格:高め", "距離:遠い", "曲がりに厳しい"]
},
direct: {
key: "direct",
icon: "🧺",
name: "直売所で販売",
desc: "お客さんと直接やりとり。天気や休日かどうかで売れ行きが大きく変わる。",
meta: ["価格:高いことも", "距離:とても近い", "売れ残りリスクあり"]
},
factory: {
key: "factory",
icon: "🏭",
name: "漬物工場に出荷",
desc: "曲がりキュウリも全部買ってくれるが、単価は安め。",
meta: ["価格:低め", "距離:中くらい", "ロスほぼなし"]
}
};
var weathers = [
{ label: "快晴", effectDemand: 0.15 },
{ label: "晴れ", effectDemand: 0.1 },
{ label: "くもり", effectDemand: 0.0 },
{ label: "小雨", effectDemand: -0.05 },
{ label: "大雨", effectDemand: -0.1 },
{ label: "猛暑", effectDemand: 0.05 }
];
var snsLevels = [
{ label: "バズっている(テレビ・SNSで話題)", bonus: 0.25 },
{ label: "そこそこ話題", bonus: 0.1 },
{ label: "普通", bonus: 0.0 },
{ label: "あまり話題になっていない", bonus: -0.05 }
];
// ====== ユーティリティ関数 ======
function randRange(min, max) { return Math.random() * (max - min) + min; }
function randInt(min, max) { return Math.floor(randRange(min, max + 1)); }
function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function formatYen(num) { return Math.round(num).toLocaleString("ja-JP") + " 円"; }
// ====== シナリオ生成 ======
function createRandomScenario() {
var weather = pickRandom(weathers);
var sns = pickRandom(snsLevels);
var harvestKg = randInt(300, 900);
var qualityScore = Math.random() * 0.4 + 0.5; // 0.5〜0.9
var curveRate = Math.random() * 0.35; // 0〜0.35
var baseDemand = 0.5 + weather.effectDemand + sns.bonus;
baseDemand += (qualityScore - 0.7) * 0.5;
baseDemand = Math.min(1.0, Math.max(0.1, baseDemand));
var demandLabel = (baseDemand >= 0.75) ? "かなり高い"
: (baseDemand >= 0.55) ? "高め"
: (baseDemand >= 0.4) ? "ふつう"
: "やや低い";
var qualityLabel = (qualityScore >= 0.82) ? "ピカピカ A 品多め"
: (qualityScore >= 0.7) ? "A〜B 品まじり"
: "ちょっと傷あり B 品多め";
var curveLabel = (curveRate <= 0.08) ? "ほぼまっすぐ"
: (curveRate <= 0.2) ? "少し曲がりが混ざる"
: "かなり曲がり多め";
return {
weather: weather.label,
harvestKg: harvestKg,
qualityScore: qualityScore,
qualityLabel: qualityLabel,
curveRate: curveRate,
curveLabel: curveLabel,
demand: baseDemand,
demandLabel: demandLabel,
snsLabel: sns.label
};
}
// ====== 利益計算(AIっぽいルール) ======
function calcProfit(planKey, s) {
var harvestKg = s.harvestKg;
var qualityScore = s.qualityScore;
var curveRate = s.curveRate;
var demand = s.demand;
var pricePerKg = 0;
var sellRate = 0;
var costPerKg = 0;
switch (planKey) {
case "supermarket":
pricePerKg = 140 + 40 * qualityScore + 20 * demand;
sellRate = 0.8 + 0.1 * demand - 0.1 * curveRate;
costPerKg = 5;
break;
case "market":
pricePerKg = 150 + 60 * qualityScore + 30 * demand;
sellRate = 0.9 + 0.05 * demand - 0.2 * curveRate;
costPerKg = 15;
break;
case "direct":
pricePerKg = 160 + 50 * qualityScore + 40 * demand;
sellRate = 0.6 + 0.3 * demand - 0.05 * curveRate;
costPerKg = 3;
break;
case "factory":
pricePerKg = 110 + 10 * qualityScore + 5 * demand;
sellRate = 1.0;
costPerKg = 8;
break;
default:
return { profit: 0, pricePerKg: 0, soldKg: 0, wastedKg: harvestKg };
}
sellRate = Math.min(1.0, Math.max(0.0, sellRate));
var soldKg = harvestKg * sellRate;
var wastedKg = harvestKg - soldKg;
var revenue = soldKg * pricePerKg;
var cost = harvestKg * costPerKg;
var profit = revenue - cost;
return { profit: profit, pricePerKg: pricePerKg, soldKg: soldKg, wastedKg: wastedKg };
}
function computeAiChoice() {
if (!state.scenario) return null;
var best = null;
var keys = Object.keys(plans);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var result = calcProfit(key, state.scenario);
if (!best || result.profit > best.profit) {
best = { planKey: key, profit: result.profit, pricePerKg: result.pricePerKg, soldKg: result.soldKg, wastedKg: result.wastedKg };
}
}
state.aiChoice = best;
return best;
}
// ====== UI 更新 ======
function renderScenario() {
var scenarioArea = document.getElementById("scenarioArea");
var scenarioHint = document.getElementById("scenarioHint");
if (!state.scenario) {
scenarioArea.innerHTML = "";
scenarioHint.innerHTML = "🔍 「新しい一日をはじめる」を押して、今日の条件を決めてみよう。";
return;
}
var s = state.scenario;
scenarioArea.innerHTML = `
<div class="scenario-item"><div class="scenario-label">天気</div><div class="scenario-value">☀️ ${s.weather}</div></div>
<div class="scenario-item"><div class="scenario-label">収穫量(きょう)</div><div class="scenario-value">${s.harvestKg.toLocaleString("ja-JP")} kg</div></div>
<div class="scenario-item"><div class="scenario-label">品質</div><div class="scenario-value">${s.qualityLabel}</div></div>
<div class="scenario-item"><div class="scenario-label">曲がりキュウリの割合</div><div class="scenario-value">${Math.round(s.curveRate * 100)} %(${s.curveLabel})</div></div>
<div class="scenario-item"><div class="scenario-label">需要レベル</div><div class="scenario-value">📈 ${s.demandLabel}</div></div>
<div class="scenario-item"><div class="scenario-label">世の中の話題</div><div class="scenario-value">📰 ${s.snsLabel}</div></div>
`;
scenarioHint.innerHTML = `
✅ 条件が決まったら、<span class="hint-highlight">右側の出荷先カード</span>をクリックしてみよう。<br>
AIきゅうりモデルは、これらの情報をもとに「利益が最大になる出荷先」をこっそり計算しています。
`;
}
function renderPlans() {
var planList = document.getElementById("planList");
planList.innerHTML = "";
Object.keys(plans).map(function(k){ return plans[k]; }).forEach(function(plan) {
var div = document.createElement("div");
div.className = "plan-card";
var metaHtml = plan.meta.map(function(m) { return '<span class="pill">' + m + "</span>"; }).join("");
div.innerHTML = `
<div class="plan-title"><span class="icon">${plan.icon}</span><span>${plan.name}</span></div>
<div class="plan-desc">${plan.desc}</div>
<div class="plan-meta">${metaHtml}</div>
`;
div.onclick = function() { handlePlanClick(plan.key); };
planList.appendChild(div);
});
}
function handlePlanClick(planKey) {
if (!state.scenario) {
var emptyResult = document.getElementById("emptyResult");
emptyResult.textContent = "まずは「新しい一日をはじめる」で畑の状況を決めてから出荷先を選んでね。";
emptyResult.style.color = "#c0392b";
return;
}
var resultArea = document.getElementById("resultArea");
var emptyResult = document.getElementById("emptyResult");
emptyResult.style.display = "none";
resultArea.style.display = "block";
var userResult = calcProfit(planKey, state.scenario);
var aiChoice = state.aiChoice || computeAiChoice();
var userPlan = plans[planKey];
var aiPlan = plans[aiChoice.planKey];
var verdictText = "";
var verdictClass = "";
if (Math.round(userResult.profit) > Math.round(aiChoice.profit)) {
verdictText = "あなたの勝ち! AIよりうまい出荷戦略です 🎉";
verdictClass = "win";
} else if (Math.round(userResult.profit) < Math.round(aiChoice.profit)) {
verdictText = "AIの勝ち! 機械学習モデルの読みもなかなかやります 🤖";
verdictClass = "lose";
} else {
verdictText = "引き分け! AIと同じくらいの読みです 🤝";
verdictClass = "draw";
}
var userWasteRate = (userResult.wastedKg / state.scenario.harvestKg) * 100;
var aiWasteRate = (aiChoice.wastedKg / state.scenario.harvestKg) * 100;
resultArea.innerHTML = `
<div class="result-row"><span class="result-label">あなたの選択:</span><span class="result-highlight">${userPlan.icon} ${userPlan.name}</span></div>
<div class="result-row"><span>売れた量:${Math.round(userResult.soldKg)} kg / ロス:${Math.round(userResult.wastedKg)} kg(約 ${Math.round(userWasteRate)}%)</span></div>
<div class="result-row"><span>平均単価:${Math.round(userResult.pricePerKg)} 円/kg / 利益:<strong>${formatYen(userResult.profit)}</strong></span></div>
<div class="result-row" style="margin-top:6px;"><span class="result-label">AIきゅうりモデルのおすすめ:</span><span class="result-highlight">${aiPlan.icon} ${aiPlan.name}</span></div>
<div class="result-row"><span>売れた量:${Math.round(aiChoice.soldKg)} kg / ロス:${Math.round(aiChoice.wastedKg)} kg(約 ${Math.round(aiWasteRate)}%)</span></div>
<div class="result-row"><span>平均単価:${Math.round(aiChoice.pricePerKg)} 円/kg / 利益:<strong>${formatYen(aiChoice.profit)}</strong></span></div>
<div class="compare">
<span class="${verdictClass}">${verdictText}</span><br>
利益の差は <strong>${formatYen(userResult.profit - aiChoice.profit)}</strong> でした。
</div>
<div class="ai-hint">
💡 <strong>AIきゅうりモデルのイメージ</strong><br>
<code>天気, 収穫量, 品質, 曲がり率, 需要レベル</code> を受け取り、<br>
「どこに出荷したら利益が最大になるか」を予測する設定です。<br>
今回は <strong>シンプルな計算ルール</strong> で真似しています。
</div>
`;
}
function newDay() {
state.day += 1; // ★ここでDayを進める
state.scenario = createRandomScenario();
state.aiChoice = null;
document.getElementById("dayText").textContent = "Day " + state.day;
renderScenario();
var resultArea = document.getElementById("resultArea");
var emptyResult = document.getElementById("emptyResult");
resultArea.style.display = "none";
resultArea.innerHTML = "";
emptyResult.style.display = "block";
emptyResult.style.color = "#566";
emptyResult.textContent = "条件を確認して、出荷先カードをクリックしてみよう。";
computeAiChoice();
}
function init() {
renderPlans();
renderScenario();
}
</script>
</body>
</html>
仕組みをもう少しだけ解説
1) 今日の状況をランダムに作る
createRandomScenario() が、天気やSNS話題などをランダムに選び、需要を計算しています。
ここが「毎回ちがう問題になる」ポイントです。
2) 出荷先ごとのルールで利益を計算する
calcProfit(planKey, s) のswitchの部分が肝です。
「市場は曲がりに厳しい」「工場は全部売れる」などの特徴を、式で表現しています。
3) AIは“全パターン試して一番良いものを選ぶ”
computeAiChoice() は、4つの出荷先を全部計算して、利益が最大のものを選ぶだけ。
でもこれだけでも「AIっぽい」動きになります。
おわりに
このミニゲームは、農業のリアルな悩み(どこに出すか)を、プログラミングの題材に落とし込んだ例です。
「式でルールを作る → シミュレーションする → 比較して意思決定する」は、農業だけでなく色々な分野で使えます。
