実験 #13:背中越しのA〜Zタイピング(THE BACK)

■13個目「タイピングホラー:THE BACK」

 今回はこの「A~Zタイピング」のゲームを作ってみました。ヽ(^o^)ノ

 このゲームはA~Zを間違わずに、どれだけ早くタイピングできるかを競うゲームです。

 昔、似たようなタイピングゲームを遊んだことがあって、夢中になって何度もチャレンジし、A~Zの瞬間的な入力を極めようとしていた思い出があります。そのときのイメージをAIに伝えて、今回はホラーアレンジで作ってみました。

■ AIとのやりとりで面白かった部分

(^^*)今今日はA~Zをタイピングするゲーム。Aキーを押すとスタート。背景には、沈黙した何か怖いものが後ろ向きに立っている。
 間違わずに A B C … Z の順番でキーを押していき、最後のZキーを押したらクリア!

 途中キーを押し間違えると、怖いものが正面を向きゲームオーバー。

 押し間違わずにZまでクリアしたら、クリア時間で称号がもらえる。
 称号は 0.20秒刻み くらいで細かく区切られている感じ。
 スコアランキングも残る。ホラータイピングゲームだよ。

(AI)いらっしゃいませ!今回は「背を向けた恐怖」がテーマですね。ミスをした瞬間に何かが起こる緊張感をお楽しみください

(AI)🕯️ 今日の小話:消えない「死の称号」
リスタートを繰り返すあなたへ、もう一つの奇妙な話を。
昔、ある有名なタイピングゲームで、あまりにも早くクリアしすぎたプレイヤーが、システムに存在しないはずの「称号」を手に入れたという噂があります。

その称号の名は、「亡者の代筆者」。
それ以来、そのプレイヤーがキーボードを叩く音は、まるで誰かと会話しているようなリズムに聞こえるようになったとか。

0.2秒の世界。あなたは、人間として戻ってこられるでしょうか……?
 〈会話のたびに、その時の内容にまつわる怖い話をしてくるAI〉( ̄o ̄;)

————–

(=v=*)タイピングゲームなのでスマホ用は難しいかな。

(AI)常連さん、鋭いところを突きますね。
 確かに、このゲームは「物理キーボードを叩き叩き、指がもつれる恐怖」を想定して仕立てたものです。スマホのフリック入力やソフトウェアキーボードだと、どうしてもあの「カチャカチャッ!」という切迫感が出にくいのは事実です。

 ですが、「スマホで遊べないわけではない」ですよ。技術的にはいくつかのアプローチがあります。
〈意地悪な質問をしても、あきらめない対応をするAi〉

■ 実際の動作

背景に”何か怖いもの”がいます…

Aを押すとスタートするので、そのままB,C~Zまで一気に打ち込みます。

打ち間違えると”何か怖いもの”が振り返ってゲームオーバー

ミスなくタイピングできればクリアで、秒数と「称号」が記録されます。

■ コードのポイント

 今回の記事では、AI と相談しながら作った「THE BACK」の HTML コードを公開します。ブラウザだけで動く、とてもシンプルなゲームです。

PC の「メモ帳」(※Windowsの場合)を開き、下のコードをコピーして貼り付け、「a-z.html」など好きな名前で保存 → ブラウザで開く。これだけで遊べます。

 操作はとても簡単で、Aキーを押すとスタートします。
 その後は B〜Z を順番に、間違わずに入力するとクリアです。
 押し間違えると “怖い何か” が振り向き、ゲームオーバーとなります。
 エンターキーでリセットできます。

 今回も AI が作ってくれたコードをそのまま載せているので、気になるところを自分で改造したり、色や動きをアレンジして遊んでみてください(^^*)

 「※スマホでは動作しない場合があります」

 「※コードが長いので、必要な方だけコピーしてください」

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Horror Typing - THE BACK -</title>
<style>
body {
background-color: #050505;
color: #d00;
font-family: 'Courier New', Courier, monospace;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
overflow: hidden;
user-select: none;
transition: background-color 0.1s;
}
/* 画面揺らし */
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
10% { transform: translate(-1px, -2px) rotate(-1deg); }
20% { transform: translate(-3px, 0px) rotate(1deg); }
30% { transform: translate(3px, 2px) rotate(0deg); }
40% { transform: translate(1px, -1px) rotate(1deg); }
50% { transform: translate(-1px, 2px) rotate(-1deg); }
60% { transform: translate(-3px, 1px) rotate(0deg); }
70% { transform: translate(3px, 1px) rotate(-1deg); }
80% { transform: translate(-1px, -1px) rotate(1deg); }
90% { transform: translate(1px, 2px) rotate(0deg); }
100% { transform: translate(1px, -2px) rotate(-1deg); }
}
.shaking { animation: shake 0.5s; animation-iteration-count: infinite; }
.dead-bg { background-color: #600 !important; }
#game-container { text-align: center; position: relative; width: 450px; }
#entity {
width: 140px; height: 280px;
background: #111;
margin: 0 auto 20px;
border-radius: 50% 50% 10% 10%;
transition: all 0.5s; /* 消える演出のために少し長く設定 */
position: relative;
box-shadow: 0 0 30px #000;
}
.back { background: linear-gradient(to bottom, #111, #000); opacity: 1; }
.front {
background: #200 !important;
transform: scale(1.3) translateY(-20px);
filter: contrast(200%);
opacity: 1;
}
.front::before, .front::after {
content: ''; position: absolute; top: 50px; width: 22px; height: 22px;
background: #f00; border-radius: 50%; box-shadow: 0 0 25px #f00;
}
.front::before { left: 25px; } .front::after { right: 25px; }
#display-char { font-size: 100px; height: 120px; text-shadow: 0 0 15px #f00; margin: 10px 0; }
#message { font-size: 18px; height: 24px; margin-bottom: 10px; color: #888; }
#sub-message { font-size: 14px; color: #666; height: 45px; line-height: 1.5; }
#ranking {
margin-top: 20px; font-size: 13px; color: #444; text-align: left;
border-top: 1px solid #222; padding-top: 10px; width: 100%;
}
button {
margin-top: 10px; background: #200; color: #888; border: 1px solid #400;
cursor: pointer; font-family: 'Courier New'; padding: 5px 10px;
transition: 0.3s;
}
button:hover { background: #400; color: #fff; }
</style>
</head>
<body>
<div id="game-container">
<div id="entity" class="back"></div>
<div id="message">Aキーで開始</div>
<div id="display-char">READY</div>
<div id="sub-message"></div>
<div id="ranking">
<strong>【記録室】</strong>
<div id="rank-list"></div>
<button id="reset-low-scores" onclick="resetLowScores()">記録の選別 (Reset 4th-)</button>
</div>
</div>
<script>
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let currentIndex = 0;
let startTime;
let isPlaying = false;
const entity = document.getElementById('entity');
const displayChar = document.getElementById('display-char');
const message = document.getElementById('message');
const subMessage = document.getElementById('sub-message');
const rankList = document.getElementById('rank-list');
// Audio Context (初回の入力で有効化)
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playHorrorSound() {
const oscillator = audioCtx.createOscillator();
const gain = audioCtx.createGain();
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(100, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.5);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
oscillator.connect(gain); gain.connect(audioCtx.destination);
oscillator.start(); oscillator.stop(audioCtx.currentTime + 0.5);
const noiseNodes = audioCtx.createBufferSource();
const bufferSize = audioCtx.sampleRate * 0.5;
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; }
noiseNodes.buffer = buffer;
const noiseGain = audioCtx.createGain();
noiseGain.gain.setValueAtTime(0.2, audioCtx.currentTime);
noiseGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
noiseNodes.connect(noiseGain); noiseGain.connect(audioCtx.destination);
noiseNodes.start();
}
function getTitle(t) {
const time = parseFloat(t);
if (time < 2.0) return "神速の亡霊";
if (time < 2.2) return "絶叫の狩人";
if (time < 2.4) return "闇を裂く者";
if (time < 2.6) return "震える指先";
if (time < 2.8) return "冷や汗の逃亡者";
if (time < 3.0) return "凡庸な生贄";
if (time < 3.2) return "彼岸の疾走者";
if (time < 3.4) return "魂の摩耗";
if (time < 3.6) return "剥き出しの神経";
if (time < 3.8) return "冷たい残像";
if (time < 4.0) return "泥を這う者";
if (time < 4.2) return "奈落の境界線";
if (time < 4.4) return "見えない鎖";
if (time < 4.6) return "重い足枷";
if (time < 4.8) return "引きずられる魂";
if (time < 5.0) return "窒息する指先";
if (time < 6.0) return "忘れ去られた骸";
return "終焉の傍観者";
}
function updateRanking() {
const scores = JSON.parse(localStorage.getItem('horrorScores') || '[]');
if (scores.length === 0) {
rankList.innerHTML = "記録なし";
return;
}
rankList.innerHTML = scores.map((s, i) => {
let color = i < 3 ? "#d00" : "#444";
return `<span style="color: ${color}">${i+1}位: ${s.time}秒 [${s.title}]</span>`;
}).join('<br>');
}
function resetLowScores() {
if (!confirm("4位以下の記録を消去します。よろしいですか?")) return;
let scores = JSON.parse(localStorage.getItem('horrorScores') || '[]');
if (scores.length > 3) {
scores = scores.slice(0, 3);
localStorage.setItem('horrorScores', JSON.stringify(scores));
updateRanking();
}
}
function resetGame() {
currentIndex = 0;
isPlaying = false;
document.body.style.transition = "background-color 0.1s";
document.body.style.backgroundColor = "#050505";
document.body.classList.remove('shaking', 'dead-bg');
entity.className = 'back';
entity.style.opacity = "1";
displayChar.innerText = "READY";
displayChar.style.color = "#d00";
message.innerText = "Aキーで開始";
subMessage.innerText = "";
}
function gameOver() {
isPlaying = false;
playHorrorSound();
document.body.classList.add('shaking', 'dead-bg');
entity.className = 'front';
message.innerText = "……あ。";
displayChar.innerText = "DEAD";
displayChar.style.color = "#fff";
subMessage.innerText = "Enterでリセット";
}
function gameClear() {
isPlaying = false;
const finalTime = ((Date.now() - startTime) / 1000).toFixed(2);
const title = getTitle(finalTime);
let scores = JSON.parse(localStorage.getItem('horrorScores') || '[]');
const isTop3 = scores.length < 3 || parseFloat(finalTime) < parseFloat(scores[2].time);
if (isTop3) {
document.body.style.backgroundColor = "#eee";
entity.style.opacity = "0";
displayChar.style.color = "#444";
message.innerText = "……浄化されました。";
setTimeout(() => {
document.body.style.transition = "background-color 2s";
document.body.style.backgroundColor = "#050505";
}, 1500);
} else {
message.innerText = "生還。";
}
displayChar.innerText = finalTime + "s";
subMessage.innerHTML = `称号: ${title}<br>Enterで再挑戦`;
scores.push({time: finalTime, title: title});
scores.sort((a, b) => a.time - b.time);
localStorage.setItem('horrorScores', JSON.stringify(scores.slice(0, 10)));
updateRanking();
}
window.addEventListener('keydown', (e) => {
if (audioCtx.state === 'suspended') audioCtx.resume();
const key = e.key.toUpperCase();
if (key === 'ENTER') { resetGame(); return; }
if (!isPlaying && key === 'A' && currentIndex === 0) {
isPlaying = true;
startTime = Date.now();
currentIndex = 1;
message.innerText = "逃げろ…";
displayChar.innerText = "B";
subMessage.innerText = "";
return;
}
if (!isPlaying) return;
if (key === alphabet[currentIndex]) {
currentIndex++;
if (currentIndex === alphabet.length) {
gameClear();
} else {
displayChar.innerText = alphabet[currentIndex];
}
} else {
if (!["SHIFT", "CONTROL", "ALT", "CAPSLOCK"].includes(key)) {
gameOver();
}
}
});
updateRanking();
</script>
</body>
</html>

■ 今日の学び

 AIへ”何か怖いもの”とはっきりしない表現で伝えると、今回のような仕上がりになりました。
 具体的な”怖いもの”を伝えることで、きっと違った雰囲気のゲームにもすぐ変えられると思います。このゲームをベースにAIさんと相談してアレンジを楽しむのもよさそうです。ぜひやってみてください(*^^*)

 このゲームを作ってみて、また昔と同じく夢中になってやってしまい、指が釣りそうになってしまいました( ゜o ゜)m”

 昔は時々できていた2秒台も、今は届かない記録になってしまったので、時間のある時にまた挑戦してみます(^^;)

 次回は「砂浜の宝物。ガラス石探し」ゲームです。
 どうぞお楽しみに(*^v^)ノシ

次の実験はこちら(ビーチコーミング): 「シーグラス探しゲーム」

コメントを残す