■43個目「しょうが君のファイヤースキー」
こんにちは、AIと作る100のゲーム実験室の みー です。(^_^)/
今回は、雪山を颯爽と滑り降りるタイムアタックゲームを作ってみました。
スキー板を履いて雪山に挑むのは……“しょうが君” です!
しょうが君は、笑顔と冒険者風の帽子がトレードマーク。
ポカポカ温まる「生姜」の力で、冷たい雪山もへっちゃらなベテラン冒険者なんです。
今回こだわったのは、雪山ならではのハラハラ感と、
それを吹き飛ばす爽快感です。(=v=*)
「障害物を避けるだけじゃなくて、たまには一気に蹴散らせたら気持ちいいだろうなぁ」
そんな遊び心から、ちょっと変わったアイテムやお邪魔キャラを登場させてみました。
コースの途中には、こんな仕掛けが待っています。
① 赤いバケツを被って左右にずりずり動く、愛嬌たっぷりの「雪だるま」 ② 取ると一定時間無敵になれる、ポカポカの「🔥」アイテム
🔥アイテムを取ると「ファイヤーしょうが君」に変身! 木や雪だるまをド派手に吹き飛ばしながら進めます。
雪煙を上げて斜面を駆け抜ける姿は、
見ているだけでもスカッとします。o(*>v<)o”
そして、猛スピードのまま落ちないように、
川の手前のゴールゾーンでピタッと止まれると……
雪の妖精「シマエナガ」たちが集まってきて、
豪華な紙吹雪でお祝いしてくれます。
クリアタイムと一緒に、シマエナガたちからの素敵な祝福メッセージもランダムで見られます。
どんな言葉が飛び出すか、ぜひ遊んで確かめてみてください。
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^)今日もゲームを作ります。
今回は HTML で動くスキーゲーム。
画面上部にプレイヤーがいて、自動で滑り下りていきます。
途中には木やでこぼこがランダムで出現して、ぶつかったり転倒するとゲームオーバー。 滑り下りた距離がスコアになり、ランキングもあります。
直線で滑るとスピードがどんどん上がって転倒しやすくなるので、
「安定度メーター」を用意して、でこぼこに当たったりスピードが上がると不安定に。
一定ラインを超えると転倒します。 スピードを落としたり時間が経つと、少しずつ回復していきます。
操作はマウスで左右のエッジを利かせて曲がる&減速、
スペースキーでボーゲン減速。
プレイヤーは雪山でも元気なしょうが君。
しょうが君は、縦長楕円の胴体に満面の笑顔、冒険者風の帽子。 手足もしょうがで、スキーセットを装備しています。
アイテムとして「🔥」が稀に落ちていて、取ると“ファイヤーしょうが君”に変身。
7秒間は木に当たってもゲームオーバーにならず、安定度も0のまま。
最終地点には川があり、落ちないようにギリギリで止まるとゴール。
シマエナガたちが祝福してくれます。
川に落ちたらゲームオーバー。
リトライもできます。
AI: 雪山を滑走するしょうが君のスキーゲームだね。
生姜に含まれる「ジンゲロール」は、加熱や乾燥で「ショウガオール」に変化して、 体を芯から温める力がグッと強くなるんだ。
だから、アイテムを取って“ファイヤーしょうが君”になる設定は、 理科や栄養学の観点から見てもすごく理にかなっているよ。
みー:(^_^*) 雪だるまを出しましょう。
背景の雪に紛れないように存在感があって、 愛嬌のあるイメージで。
ランダム出現で、左右にゆっくりずりずり動くお邪魔キャラ。
ぶつかったら吹き飛んで、安定度メーターにも影響する感じで。
あと、ゴールエリアを150%ほど拡大させよう。
AI: 雪だるまのお邪魔キャラ、いいね。
背景に埋もれないように存在感を出しつつ、愛嬌もキープしておいたよ。
ぶつかった時はド派手に吹き飛ぶようにして、安定度にも影響するよう調整済み。
ゴールエリアもご要望通りしっかり広げておいた。
川の手前のドキドキ感は残しつつ、ちょっと余裕ができたはず。
みー:(つv<*) ゴールした時のシマエナガの祝福を、もう少し豪華にしてみよう! 祝福の言葉も5種類ぐらい、素敵なのをお願い。
AI: シマエナガの祝福を豪華にしたいって?任せてくれ!
あの子たちの可愛いお祝いシーンを、もっと心温まる演出にアップデートしたよ。
ゴール時のセリフも5種類、ロマンチックで素敵なのを用意しておいた。 どの言葉が聞けるかは、君の滑り次第だ。
さあ、特製アップデート版 v8「シマエナガ・フェスティバル」。 音量を少し上げて、最速タイムと感動のゴールを目指して滑り降りてくれ!
■ 実際の動作
まずは、AIと相談しながら作ってもらった 初期状態のゲーム画面 です。
今回はゲーム案の段階でかなり詳しく相談したので、最初から整った形になっています。

(画像:AIが最初に作った基本のベース画面)
こちらが 修正版のスタート画面 です。

(画像:仕上がり版スタート画面)
ゲームが始まったら、しょうが君をマウスで操作します。
スピードを下げたいときは、スペースキーでボーゲン減速ができます。

(画像:仕上がりのゲーム画面)
ゴール地点でスピードを出しすぎていると、
そのまま川に飛び込んでしまいリタイアとなります。

(画像:川に飛び込むしょうが君)
■ コードのポイント
今回の記事では、AI と相談しながら作った 「しょうが君のファイヤースキー」 の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。
【遊ぶまでの手順】
1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「syougasuki.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。
🌿 遊び方・操作方法 🌿
遊び方はとっても直感的でカンタンです。(^_^)
⛄ 基本の操作
・マウスを左右に動かす しょうが君がマウスに合わせて左右に移動します。 大きく動かすとスキーの「エッジ」が効いて、少し減速しながら雪煙を上げて滑ります。
・スペースキーを押す 押している間は、スキー板を「ハの字」にする“ボーゲン”の姿勢になり、急ブレーキができます。
⛄ 気をつけるポイント
画面右上には、しょうが君の 安定度メーター があります。
直滑降でスピードが出すぎたり、 コース上の「でこぼこ」や、無敵状態でない時に「雪だるま」にぶつかると、 バランスを崩してメーターが減ってしまいます。(>_<)
メーターがゼロになると転倒してしまうので、 「危ない!」と思ったらスペースキーでしっかり減速して、態勢を立て直してくださいね。
スピードを落とせば、安定度は徐々に回復していきます。(=v=*)
⛄ ゴールを目指して!
木を避け、雪だるまを蹴散らしながら進んでいくと、 最終地点には冷たい 川 が待ち構えています。
川の手前にある緑色の GOAL ZONE でしっかり止まることができれば、見事クリア!(´▽`*)
勢い余って川にドボン……となるとリタイアになってしまうので気をつけてくださいね。
最速タイムはトップ3まで記録されます。 ぜひ何度も挑戦して、シマエナガたちと一緒に最高のお祝いを味わってみてください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>しょうが君のファイヤースキー</title> <style> body { margin: 0; padding: 0; background-color: #2c3e50; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; } #gameWrapper { position: relative; } #gameContainer { position: relative; width: 500px; height: 700px; background-color: #fffafa; box-shadow: 0 0 20px rgba(0,0,0,0.5); overflow: hidden; } canvas { display: block; } #uiLayer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } .hud { position: absolute; padding: 10px; font-weight: bold; font-size: 16px; color: #333; } #scoreDisplay { top: 10px; left: 10px; background: rgba(255,255,255,0.7); border-radius: 8px; padding: 10px; min-width: 150px; } #timeVal { font-size: 20px; } #rankingContainer { margin-top: 5px; border-top: 1px solid #ccc; padding-top: 5px; } .ranking-title { font-size: 18px; margin-bottom: 3px; color: #e67e22; } .ranking-list { list-style: none; padding: 0; margin: 0; font-size: 24px; font-weight: bold; } .ranking-item { margin-bottom: 2px; } .rank-1 { color: #f1c40f; } .rank-2 { color: #95a5a6; } .rank-3 { color: #d35400; } .rank-num { margin-right: 5px; } .small-text { font-size: 14px; color: #555; font-weight: normal; } #stabilityContainer { top: 10px; right: 10px; width: 150px; text-align: right; background: rgba(255,255,255,0.7); border-radius: 8px; padding: 10px; } #stabilityBar { width: 100%; height: 15px; background: #ddd; border: 2px solid #333; margin-top: 5px; position: relative; } #stabilityFill { height: 100%; background: limegreen; width: 100%; transition: background 0.2s, width 0.1s; } #overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.75); display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; pointer-events: auto; } #overlay h1 { font-size: 36px; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); } #overlay p { font-size: 18px; margin-bottom: 20px; text-align: center; line-height: 1.5; } button { padding: 12px 35px; font-size: 20px; cursor: pointer; background-color: #e74c3c; color: white; border: none; border-radius: 5px; font-weight: bold; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: transform 0.1s, background-color 0.2s; } button:hover { background-color: #c0392b; transform: translateY(-2px); } button:active { transform: translateY(0); } .hidden { display: none !important; } </style></head><body><div id="gameWrapper"> <div id="gameContainer"> <canvas id="gameCanvas" width="500" height="700"></canvas> <div id="uiLayer"> <div id="scoreDisplay" class="hud"> タイム: <span id="timeVal">0.00</span> 秒<br> <div id="rankingContainer"> <div class="ranking-title">最速記録トップ3</div> <ul id="bestTimesList" class="ranking-list"></ul> </div> <span class="small-text">現在位置: <span id="distVal">0</span> m</span> </div> <div id="stabilityContainer" class="hud"> 安定度 <div id="stabilityBar"><div id="stabilityFill"></div></div> </div> </div> <div id="overlay"> <h1 id="resultTitle">しょうが君のスキー</h1> <p id="resultMsg">マウス左右で移動&エッジ減速<br>スペースキーでボーゲン(急減速)</p> <button id="startBtn">スタート</button> </div> </div></div><script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const gameWrapper = document.getElementById('gameWrapper'); const overlay = document.getElementById('overlay'); const startBtn = document.getElementById('startBtn'); const resultTitle = document.getElementById('resultTitle'); const resultMsg = document.getElementById('resultMsg'); const distVal = document.getElementById('distVal'); const timeVal = document.getElementById('timeVal'); const bestTimesList = document.getElementById('bestTimesList'); const stabilityFill = document.getElementById('stabilityFill'); const GOAL_DISTANCE = 10000; const GOAL_ZONE_LENGTH = 300; const MAX_SPEED = 15; const DANGER_SPEED = 8; const FIRE_DURATION = 5000; const STORAGE_KEY = 'shouga_ski_best_times_v9'; let bestTimes = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; function displayBestTimes() { bestTimesList.innerHTML = ''; for (let i = 0; i < 3; i++) { const li = document.createElement('li'); li.className = `ranking-item rank-${i+1}`; const timeStr = bestTimes[i] ? bestTimes[i].toFixed(2) + ' 秒' : '--'; li.innerHTML = `<span class="rank-num">${i+1}位:</span> <span class="rank-time">${timeStr}</span>`; bestTimesList.appendChild(li); } } displayBestTimes(); let state = 'start'; let distance = 0; let speedY = 2; let stability = 100; let lastTime = 0; let startTime = 0; let elapsedTime = 0; let shakeAmount = 0; let player = { x: 250, y: 150, width: 30, height: 50, isBraking: false, isFire: false, fireEndTime: 0, tilt: 0 }; let objects = []; let snowParticles = []; let celebrationParticles = []; let mouthParticles = []; let mouseX = 250; // ----- 音声処理 ----- let audioCtx, windGain, windFilter; function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const bufferSize = audioCtx.sampleRate * 2; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; let noiseSource = audioCtx.createBufferSource(); noiseSource.buffer = buffer; noiseSource.loop = true; windFilter = audioCtx.createBiquadFilter(); windFilter.type = 'lowpass'; windFilter.frequency.value = 100; windGain = audioCtx.createGain(); windGain.gain.value = 0; noiseSource.connect(windFilter); windFilter.connect(windGain); windGain.connect(audioCtx.destination); noiseSource.start(); } if (audioCtx.state === 'suspended') audioCtx.resume(); } function playSFX(type) { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); const now = audioCtx.currentTime; if (type === 'crash') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(10, now + 0.5); gain.gain.setValueAtTime(0.5, now); gain.gain.linearRampToValueAtTime(0.01, now + 0.5); osc.start(now); osc.stop(now + 0.5); } else if (type === 'fire') { osc.type = 'sine'; osc.frequency.setValueAtTime(600, now); osc.frequency.linearRampToValueAtTime(1200, now + 0.2); gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0.01, now + 0.2); osc.start(now); osc.stop(now + 0.2); } else if (type === 'clear') { osc.type = 'triangle'; osc.frequency.setValueAtTime(523.25, now); osc.frequency.setValueAtTime(659.25, now + 0.1); osc.frequency.setValueAtTime(783.99, now + 0.2); osc.frequency.setValueAtTime(1046.50, now+ 0.3); gain.gain.setValueAtTime(0.4, now); gain.gain.linearRampToValueAtTime(0.01, now + 1.0); osc.start(now); osc.stop(now + 1.0); } } // ----- 入力 ----- canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); mouseX = e.clientX - rect.left; }); window.addEventListener('keydown', (e) => { if (e.code === 'Space') player.isBraking = true; }); window.addEventListener('keyup', (e) => { if (e.code === 'Space') player.isBraking = false; }); startBtn.addEventListener('click', () => { initAudio(); initGame(); state = 'playing'; overlay.classList.add('hidden'); }); function initGame() { distance = 0; speedY = 2; stability = 100; shakeAmount = 0; gameWrapper.style.transform = `translate(0px, 0px)`; player.x = 250; player.isBraking = false; player.isFire = false; player.tilt = 0; objects = []; snowParticles = []; celebrationParticles = []; mouthParticles = []; const now = performance.now(); lastTime = now; startTime = now; elapsedTime = 0; } function spawnObject() { if (distance > GOAL_DISTANCE - 1000) return; if (Math.random() < 0.05) { let r = Math.random(); let type = r < 0.5 ? 'tree' : (r < 0.8 ? 'bump' : (r < 0.95 ? 'snowman' : 'fire')); let vx = type === 'snowman' ? (Math.random() - 0.5) * 2 : 0; objects.push({ type: type, x: Math.random() * (canvas.width - 40) + 20, y: canvas.height + 50, width: type === 'fire' ? 30 : 40, height: type === 'fire' ? 30 : 40, active: true, vx: vx, rotation: 0 }); } } function checkCollision(obj) { let hitX = (player.x - 10 < obj.x + obj.width/2) && (player.x + 10 > obj.x - obj.width/2); let hitY = (player.y - 20 < obj.y + obj.height/2) && (player.y + 20 > obj.y - obj.height/2); return hitX && hitY; } function createSnowDust(x, y, intensity) { for(let i=0; i<intensity; i++) { snowParticles.push({ x: x + (Math.random() - 0.5) * 30, y: y + 20 + (Math.random() - 0.5) * 10, vx: (Math.random() - 0.5) * 4, vy: -Math.random() * 3 - 1, size: Math.random() * 4 + 2, life: 20 }); } } function createCelebrationParticles(riverY) { const colors = ['#f1c40f', '#e74c3c', '#3498db', '#2ecc71', '#e0e0e0']; for(let i=0; i<150; i++) { celebrationParticles.push({ x: Math.random() * canvas.width, y: riverY - GOAL_ZONE_LENGTH + Math.random() * (GOAL_ZONE_LENGTH + player.y), vx: (Math.random() - 0.5) * 15, vy: -20 - Math.random() * 20, size: Math.random() * 6 + 2, color: colors[Math.floor(Math.random() * colors.length)], life: 100 + Math.random() * 100 }); } } function createMouthParticles(x, y) { for(let i=0; i<10; i++) { mouthParticles.push({ x: x + (Math.random() - 0.5) * 5, y: y + (Math.random() - 0.5) * 5, vx: (Math.random() - 0.5) * 2, vy: -Math.random() * 2 - 1, size: Math.random() * 3 + 1, life: 30 }); } } function update(dt, now) { if (shakeAmount > 0) { shakeAmount *= 0.9; let sx = (Math.random() - 0.5) * shakeAmount; let sy = (Math.random() - 0.5) * shakeAmount; gameWrapper.style.transform = `translate(${sx}px, ${sy}px)`; if (shakeAmount < 0.5) { shakeAmount = 0; gameWrapper.style.transform = `translate(0px, 0px)`; } } if (state !== 'playing') { if (windGain) windGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1); for(let i = celebrationParticles.length - 1; i >= 0; i--) { celebrationParticles[i].life--;celebrationParticles[i].x += celebrationParticles[i].vx;celebrationParticles[i].y += celebrationParticles[i].vy;celebrationParticles[i].vy += 0.5;celebrationParticles[i].vx *= 0.98; if(celebrationParticles[i].life <= 0) celebrationParticles.splice(i, 1); } for(let i = mouthParticles.length - 1; i >= 0; i--) { mouthParticles[i].life--;mouthParticles[i].x += mouthParticles[i].vx;mouthParticles[i].y += mouthParticles[i].vy; if(mouthParticles[i].life <= 0) mouthParticles.splice(i, 1); } if (state === 'clear' && mouthParticles.length < 5) createMouthParticles(player.x, player.y - 10); for(let i = snowParticles.length - 1; i >= 0; i--) { snowParticles[i].life--;snowParticles[i].x += snowParticles[i].vx;snowParticles[i].y += snowParticles[i].vy; if(snowParticles[i].life <= 0) snowParticles.splice(i, 1); } objects.forEach(obj => { if (obj.type === 'broken_tree' || obj.type === 'broken_snowman') { obj.x += obj.vx;obj.y += obj.vy;obj.vy += 0.5;obj.rotation += obj.rotSpeed; } }); return; } elapsedTime = (now - startTime) / 1000; if (player.isFire && now > player.fireEndTime) player.isFire = false; let diffX = mouseX - player.x;let moveX = diffX * 0.05;player.x += moveX;player.x = Math.max(20, Math.min(canvas.width - 20, player.x)); let targetTilt = Math.max(-Math.PI/6, Math.min(Math.PI/6, -diffX * 0.015)); player.tilt += (targetTilt - player.tilt) * 0.2; let edgeBrake = Math.abs(moveX) > 1 ? 0.05 : 0; if (player.isBraking) { speedY -= 0.2;createSnowDust(player.x, player.y, 3); } else { speedY += 0.05; if (Math.abs(moveX) > 2) createSnowDust(player.x, player.y, 1); } speedY -= edgeBrake;speedY = Math.max(0, Math.min(MAX_SPEED, speedY)); if (windFilter && windGain) { let targetFreq = 100 + (speedY * 50);let targetGain = (speedY / MAX_SPEED) * 0.3; if (player.isBraking) { targetFreq += 500; targetGain += 0.1; } windFilter.frequency.setTargetAtTime(targetFreq, audioCtx.currentTime, 0.1);windGain.gain.setTargetAtTime(targetGain, audioCtx.currentTime, 0.1); } if (player.isFire) stability = 100; else { if (speedY > DANGER_SPEED) stability -= (speedY - DANGER_SPEED) * 0.1;else stability += 0.2; } stability = Math.max(0, Math.min(100, stability)); if (stability <= 0) { playSFX('crash');shakeAmount = 25;gameOver("スピードの出しすぎで転倒した!");return; } distance += speedY;let remainingToRiver = GOAL_DISTANCE - distance;let riverY = player.y + remainingToRiver; if (distance >= GOAL_DISTANCE) { playSFX('crash');shakeAmount = 20;gameOver("止まれずに川に落ちてしまった!");return; } else if (distance >= GOAL_DISTANCE - GOAL_ZONE_LENGTH && speedY < 0.5) { playSFX('clear');shakeAmount = 10;createCelebrationParticles(riverY);gameClear(riverY);return; } for (let i = objects.length - 1; i >= 0; i--) { let obj = objects[i]; if (obj.type === 'broken_tree' || obj.type === 'broken_snowman') { obj.x += obj.vx; obj.y += obj.vy; obj.vy += 0.5; obj.rotation += obj.rotSpeed; } else { obj.y -= speedY; if (obj.type === 'snowman') { obj.x += obj.vx;if (obj.x < 20 || obj.x > canvas.width - 20) obj.vx *= -1; } } if (obj.active && checkCollision(obj)) { if (obj.type === 'tree') { if (player.isFire) { obj.type = 'broken_tree'; obj.active = false;obj.vx = (Math.random() > 0.5 ? 1 : -1) * (10 + Math.random() * 5); obj.vy = -10 - Math.random() * 5; obj.rotation = 0; obj.rotSpeed = (Math.random() - 0.5) * 0.5; playSFX('crash'); shakeAmount = 10; } else { playSFX('crash'); shakeAmount = 25;gameOver("木に激突した!"); return; } } else if (obj.type === 'snowman') { if (player.isFire) { obj.type = 'broken_snowman'; obj.active = false;obj.vx = (Math.random() > 0.5 ? 1 : -1) * (12 + Math.random() * 6); obj.vy = -12 - Math.random() * 6; obj.rotation = 0; obj.rotSpeed = (Math.random() - 0.5) * 0.6;playSFX('crash'); shakeAmount = 10; } else { obj.type = 'broken_snowman'; obj.active = false;obj.vx = (Math.random() > 0.5 ? 1 : -1) * (5 + Math.random() * 3); obj.vy = -8 - Math.random() * 4; obj.rotation = 0; obj.rotSpeed = (Math.random() - 0.5) * 0.4;stability -= 25; speedY *= 0.8; playSFX('crash'); shakeAmount = 10; } } else if (obj.type === 'bump') { obj.active = false;if (!player.isFire) { stability -= 25; speedY *= 0.8; shakeAmount = 10; } } else if (obj.type === 'fire') { obj.active = false; player.isFire = true;player.fireEndTime = now + FIRE_DURATION; stability = 100; playSFX('fire'); } } if (obj.y < -100 || obj.y > canvas.height + 200 || obj.x < -100 || obj.x > canvas.width + 100) { objects.splice(i, 1); } } spawnObject(); for(let i = snowParticles.length - 1; i >= 0; i--) { snowParticles[i].life--; snowParticles[i].x += snowParticles[i].vx;snowParticles[i].y += snowParticles[i].vy - speedY * 0.5; if(snowParticles[i].life <= 0) snowParticles.splice(i, 1); } distVal.innerText = Math.floor(distance / 10); timeVal.innerText = elapsedTime.toFixed(2); stabilityFill.style.width = stability + '%'; if (player.isFire) stabilityFill.style.background = 'orange';else stabilityFill.style.background = stability > 50 ? 'limegreen' : (stability > 20 ? 'yellow' : 'red'); } function draw() { ctx.fillStyle = '#fffafa';ctx.fillRect(0, 0, canvas.width, canvas.height);ctx.strokeStyle = '#e0e0e0';ctx.lineWidth = 2;ctx.beginPath(); for(let i=0; i<5; i++) { let lineY = (distance * 0.5 + i * 150) % canvas.height;ctx.moveTo(0, lineY);ctx.lineTo(canvas.width, lineY + 50); } ctx.stroke(); let remainingToRiver = GOAL_DISTANCE - distance;let riverY = player.y + remainingToRiver;let goalLineY = riverY - GOAL_ZONE_LENGTH; if (riverY < canvas.height && riverY > -1000) { ctx.fillStyle = '#3498db'; ctx.fillRect(0, riverY, canvas.width, canvas.height);ctx.fillStyle = '#2980b9'; ctx.fillRect(0, riverY + 20, canvas.width, 10); } if (goalLineY < canvas.height && riverY > 0) { ctx.fillStyle = 'rgba(46, 204, 113, 0.2)'; ctx.fillRect(0, goalLineY, canvas.width, GOAL_ZONE_LENGTH);ctx.strokeStyle = '#2ecc71'; ctx.lineWidth = 4; ctx.setLineDash([15, 10]); ctx.beginPath(); ctx.moveTo(0, goalLineY); ctx.lineTo(canvas.width, goalLineY); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = '#27ae60'; ctx.font = 'bold 20px Arial'; ctx.textAlign = 'center';ctx.fillText('GOAL ZONE (ここで止まれ!)', canvas.width / 2, goalLineY - 10); } if (remainingToRiver <= 2000 && remainingToRiver > GOAL_ZONE_LENGTH) { ctx.fillStyle = 'red'; ctx.font = 'bold 26px Arial'; ctx.textAlign = 'center'; if (Math.floor(performance.now() / 500) % 2 === 0) { ctx.fillText(`⚠️ もうすぐ川だ!残り ${Math.floor(remainingToRiver / 10)}m ⚠️`, canvas.width / 2, 80); } } objects.forEach(obj => { if (!obj.active && obj.type !== 'bump' && obj.type !== 'broken_tree' && obj.type !== 'broken_snowman') return; ctx.save(); ctx.translate(obj.x, obj.y); if (obj.type === 'tree') { ctx.fillStyle = '#8b5a2b'; ctx.fillRect(-5, 0, 10, 20);ctx.fillStyle = '#2ecc71'; ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(-15, 5); ctx.lineTo(15, 5); ctx.fill(); } else if (obj.type === 'broken_tree') { ctx.rotate(obj.rotation); ctx.fillStyle = '#8b5a2b'; ctx.fillRect(-5, -10, 10, 20);ctx.fillStyle = '#2ecc71'; ctx.beginPath(); ctx.arc(0, -10, 10, 0, Math.PI*2); ctx.fill(); } else if (obj.type === 'snowman' || obj.type === 'broken_snowman') { if (obj.type === 'broken_snowman') ctx.rotate(obj.rotation); ctx.fillStyle = '#ecf0f1'; ctx.strokeStyle = '#95a5a6'; ctx.lineWidth = 1;ctx.beginPath(); ctx.arc(0, 12, 18, 0, Math.PI*2); ctx.fill(); ctx.stroke();ctx.beginPath(); ctx.arc(0, -8, 12, 0, Math.PI*2); ctx.fill(); ctx.stroke();ctx.fillStyle = '#e74c3c'; ctx.beginPath(); ctx.moveTo(-10, -18); ctx.lineTo(10, -18); ctx.lineTo(8, -30); ctx.lineTo(-8, -30); ctx.closePath(); ctx.fill();ctx.fillStyle = '#333'; ctx.fillRect(-11, -19, 22, 2); ctx.fillStyle = '#f39c12'; ctx.fillRect(-12, -2, 24, 6); ctx.fillRect(2, -2, 6, 15); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(-5, -12, 1.5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(5, -12, 1.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#e67e22'; ctx.beginPath(); ctx.moveTo(0, -10); ctx.lineTo(3, -9); ctx.lineTo(0, -8); ctx.fill();ctx.strokeStyle = 'black'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, -8, 4, 0.2, Math.PI-0.2); ctx.stroke(); } else if (obj.type === 'bump') { ctx.fillStyle = '#ecf0f1'; ctx.strokeStyle = '#bdc3c7'; ctx.lineWidth = 2;ctx.beginPath(); ctx.ellipse(0, 0, 20, 10, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (obj.type === 'fire') { ctx.font = '24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('🔥', 0, 0); } ctx.restore(); }); snowParticles.forEach(p => { ctx.fillStyle = 'rgba(236, 240, 241, ' + (p.life / 20) + ')'; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill(); }); celebrationParticles.forEach(p => { ctx.fillStyle = p.color; ctx.globalAlpha = p.life / 100; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha = 1.0; }); mouthParticles.forEach(p => { ctx.fillStyle = '#ff69b4'; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill(); }); drawShouga(player.x, player.y, player.isFire, player.isBraking, player.tilt); if (state === 'clear') drawShimaenagas(); } function drawShouga(x, y, isFire, isBraking, tilt) { ctx.save(); ctx.translate(x, y); ctx.rotate(tilt); if (isFire) { ctx.fillStyle = 'rgba(255, 165, 0, 0.5)'; ctx.beginPath(); ctx.arc(0, 0, 40 + Math.random()*5, 0, Math.PI*2); ctx.fill(); } // [修正] 胴体の描画(斜線模様を追加) ctx.fillStyle = '#e4bc8a'; ctx.beginPath(); ctx.ellipse(0, 0, 18, 28, 0, 0, Math.PI*2); ctx.fill(); // 胴体下半分の薄い斜線模様 ctx.save(); ctx.beginPath(); ctx.ellipse(0, 0, 18, 28, 0, 0, Math.PI * 2); ctx.clip(); ctx.beginPath(); ctx.rect(-20, 0, 40, 30); ctx.clip(); ctx.strokeStyle = '#c69c6d'; ctx.lineWidth = 1; ctx.globalAlpha = 0.3; ctx.beginPath(); for (let yL = -40; yL < 60; yL += 10) { ctx.moveTo(-40, yL + 40); ctx.lineTo(40, yL - 40); } ctx.stroke(); ctx.restore(); // スキー板、手足 ctx.fillStyle = '#e74c3c'; if (isBraking) { ctx.save(); ctx.rotate(Math.PI / 8); ctx.fillRect(-25, 10, 6, 50); ctx.restore(); ctx.save(); ctx.rotate(-Math.PI / 8); ctx.fillRect(19, 10, 6, 50); ctx.restore(); } else { ctx.fillRect(-15, 10, 6, 50); ctx.fillRect(9, 10, 6, 50); } ctx.fillStyle = '#d3a167';ctx.beginPath(); ctx.ellipse(-18, 5, 5, 10, Math.PI/4, 0, Math.PI*2); ctx.fill();ctx.beginPath(); ctx.ellipse(18, 5, 5, 10, -Math.PI/4, 0, Math.PI*2); ctx.fill(); // 顔 ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(-6, -12, 2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(6, -12, 2, 0, Math.PI*2); ctx.fill(); // [修正] 口の色を赤に ctx.strokeStyle = 'red'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, -6, 8, 0, Math.PI); ctx.stroke(); // 帽子 // [修正] 帽子の上のところをもう少し四角よりの形に(ワークキャップ風) ctx.fillStyle = '#8b4513'; // 1. ツバ(ellipseはそのまま活かす) ctx.beginPath(); ctx.ellipse(0, -26, 22, 6, 0, 0, Math.PI*2); ctx.fill(); // 2. クラウン(頭頂部)を丸いarcから四角いシルエットへ ctx.beginPath(); ctx.moveTo(-15, -28); // 左下 ctx.lineTo(15, -28); // 右下 ctx.lineTo(13, -36); // 右上(少し窄める) ctx.lineTo(-13, -36); // 左上 ctx.closePath(); ctx.fill(); // 3. 少し立体感を出すための影(オプション、薄く) ctx.fillStyle = 'rgba(0,0,0,0.1)'; ctx.fillRect(-13, -36, 26, 2); ctx.restore(); } function drawShimaenagas() { ctx.save(); const positions = [ [150, player.y - 50], [350, player.y - 40], [250, player.y - 80] ]; const now = performance.now(); positions.forEach(pos => { let hopOffset = Math.sin(now / 300 + pos[0]) * 3; let wingVisible = (now / 200 + pos[1]) % 2 === 0; ctx.translate(pos[0], pos[1] + hopOffset); ctx.fillStyle = '#333'; ctx.beginPath(); ctx.ellipse(10, 10, 3, 15, -Math.PI/4, 0, Math.PI*2); ctx.fill(); if (wingVisible) { ctx.fillStyle = 'white'; ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = 1; ctx.beginPath(); ctx.ellipse(-15, 0, 10, 5, Math.PI/6, 0, Math.PI*2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.ellipse(15, 0, 10, 5, -Math.PI/6, 0, Math.PI*2); ctx.fill(); ctx.stroke(); } ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(0, 0, 15, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(-5, -2, 1.5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(5, -2, 1.5, 0, Math.PI*2); ctx.fill();ctx.fillStyle = '#f39c12'; ctx.beginPath(); ctx.moveTo(-2, 2); ctx.lineTo(2, 2); ctx.lineTo(0, 5); ctx.fill(); ctx.translate(-pos[0], -(pos[1] + hopOffset)); }); ctx.restore(); } function updateBestTimes() { bestTimes.push(elapsedTime);bestTimes.sort((a, b) => a - b);bestTimes = bestTimes.slice(0, 3); localStorage.setItem(STORAGE_KEY, JSON.stringify(bestTimes));displayBestTimes(); } function gameOver(reason) { state = 'gameover'; resultTitle.innerText = "無念のリタイア"; resultMsg.innerHTML = `${reason}<br><br>到達地点: ${Math.floor(distance / 10)} m<br>経過タイム: ${elapsedTime.toFixed(2)} 秒`; startBtn.innerText = "リトライ"; overlay.classList.remove('hidden'); } function gameClear() { state = 'clear'; updateBestTimes(); let isNewRecord = bestTimes.includes(elapsedTime); resultTitle.innerText = "感動のゴール!!"; const wonderfulMessages = ["風のように駆け抜け、伝説のシュプールを描いたね!シマエナガたちが、君の勇姿に心を奪われているよ!","見事な雪山の冒険者だ!その熱い滑りは、氷点下の世界さえも温めたよ。最高のゴールシーンだ!","白い妖精たちが舞い踊る!君の華麗なストップに、シマエナガ一同、スタンディングオベーションだよ!","奇跡のゴールイン!この美しい雪原に、君の情熱という名の新たな轍(わだち)が刻まれたね!","君の滑りは、この冷たい雪山さえも熱く燃え上がらせた!生姜の力が、今、最高の思い出となって輝いているよ!"]; let msg = wonderfulMessages[Math.floor(Math.random() * wonderfulMessages.length)] + "<br><br>"; msg += `<span style="font-size: 24px; color: #f1c40f;">クリアタイム: ${elapsedTime.toFixed(2)} 秒</span>`; if (isNewRecord) { const rank = bestTimes.indexOf(elapsedTime) + 1;const rankColors = ['#f1c40f', '#95a5a6', '#d35400']; msg += `<br><br><strong style='color: ${rankColors[rank-1]};'>★${rank}位にランクイン!おめでとう!★</strong>`; } resultMsg.innerHTML = msg; startBtn.innerText = "もう一度タイムに挑む"; setTimeout(() => { overlay.classList.remove('hidden'); }, 2000); } function gameLoop(now) { let dt = now - lastTime; lastTime = now; update(dt, now); draw(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);</script></body></html>
■ 今日の学び
R8年4月22日現在、ついに 98個目 までゲームができました。
「毎日1つ作る」を基本に楽しんできましたが、お休みの日には2つ、3つと作る日もあり、 1月24日から始めて 100日経たずに100個に届きそう です。
まだブログで紹介できているゲームは半分の50にも届いていないので、 ほかのゲームたちも早く紹介したい気持ちでいっぱいです。
種類も世界観もバラバラなゲームたちですが、 どこかの要素でゆるくつながっていて、 このブログ全体がひとつの大きなゲームのようにも思えてきました。
これからは 1,000のゲーム実験室 を目指しつつ、 ゲームのもとになる 「自分自身の経験値を高めていくこと」 も、同時進行の目標にしていきます。
座右の銘は「進歩」。 今日より少しだけ前へ。そんな気持ちで続けていきます。p(^o^)q”
■ 次回は、癒しゲームです。
疲れたあなたに、ほっと一息つける癒しのひと時を。(*=_=)ノ~*
次回は、テーマの違うステージから好きなものを選んで、
ぽよぽよと気ままにクリックするだけで楽しめる、
やさしい 「癒しゲーム」 をご紹介します。
深く考えず、ただ触れているだけで心がゆるむような、
そんな小さな癒しをお届けできればと思っています。
次回もお楽しみに。(^v^)ノシ
今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー