🏫 先生向けガイド
高校「情報Ⅰ」の(3) コンピュータとプログラミングで扱う配列・反復・条件分岐・関数・アルゴリズムを、 教科書用に書かれた例題ではなく、実際に公開されて動いているゲームのJavaScriptで読む練習問題です。 題材は2048・迷路・ブロック崩し・リズムゲーム。 どれもこのサイトで遊べて、ソースページで全文を読み、数値を書きかえてその場で動かせます。
解答は折りたたんであります。生徒に配るときはURLをそのまま渡して構いません(無料・登録不要・先生向けページには広告を出していません)。
共通テスト「情報Ⅰ」のプログラミングは、特定の言語ではなく大学入試センターの「共通テスト用プログラム表記」で出題されます。 このページの問題はその形式ではありません。過去問や試作問題の代わりにはならないので、形式に慣れる練習はセンターの公開資料で行ってください。 ここで鍛えるのは、変数の値を追う・配列の添字を追う・分岐の順序を考えるという、言語や表記によらない読解の力です。
問題を読むときの手がかりとして、主な書き方を並べます。表記の細部は年度で見直されることがあるため、正確な形は大学入試センターの公開資料で確認してください。
| やりたいこと | 共通テスト用プログラム表記(例) | JavaScript(このページのコード) |
|---|---|---|
| 変数に入れる | x = 1 | var x = 1; |
| 表示する | 表示する(x) | console.log(x); |
| もし〜なら | もし x > 0 ならば:そうでなければ: | if (x > 0) { … } else { … } |
| 回数を決めて繰り返す | i を 0 から 3 まで 1 ずつ増やしながら繰り返す: | for (var i = 0; i < 4; i++) { … } |
| 条件がなりたつ間繰り返す | x < 10 の間繰り返す: | while (x < 10) { … } |
| 配列(添字は0から) | Data = [3, 1, 2]、Data[0] | var data = [3, 1, 2];、data[0] |
| 配列の長さ | 要素数(Data) | data.length |
| 関数を定義して呼ぶ | 問題文で「関数 〇〇(x) は…を返す」と定義される | function f(x) { return …; }、f(2) |
JavaScriptの { } は「ここからここまでがひとまとまり」、; は「文の終わり」。共通テスト用プログラム表記では字下げでまとまりを表します。
2048では、1行の数を左に詰めて、となり合う同じ数を合体させます。次は2048のコードから、1行分の処理を読みやすさのため一部整理したものです(SIZE は 4)。
function slideRow(row) {
// 0 を取り除く(例: [2, 0, 2, 4] → [2, 2, 4])
var filtered = [];
for (var k = 0; k < row.length; k++) {
if (row[k] !== 0) filtered.push(row[k]);
}
var merged = [];
var scoreGain = 0;
for (var i = 0; i < filtered.length; i++) {
if (i + 1 < filtered.length && filtered[i] === filtered[i + 1]) {
var newVal = filtered[i] * 2;
merged.push(newVal);
scoreGain += newVal;
i++; // (ア)
} else {
merged.push(filtered[i]);
}
}
while (merged.length < SIZE) merged.push(0); // 右側を 0 で埋める
return { row: merged, scoreGain: scoreGain };
}
row = [2, 0, 2, 4] のとき、返される row と scoreGain を答えなさい。row = [4, 4, 4, 4] のとき、返される row と scoreGain を答えなさい。i++ を削除すると、row = [2, 0, 2, 4] の結果はどう変わるか。また、それはゲームのルールとしてなぜ困るか説明しなさい。(1) filtered = [2, 2, 4]。i = 0 で filtered[0] === filtered[1] なので 4 を入れ、i++ で i = 1、ループの i++ で i = 2。filtered[2] = 4 はとなりがないのでそのまま。merged = [4, 4] → 0で埋めて row = [4, 4, 0, 0]、scoreGain = 4。
(2) filtered = [4, 4, 4, 4]。i = 0 で合体して 8、i = 2 で合体して 8。row = [8, 8, 0, 0]、scoreGain = 16。[16, 0, 0, 0] にはならないことに注意(1回の移動で二重に合体しない)。
(3) (ア)がないと、i = 0 で 4 を入れたあと i = 1 に進み、filtered[1] = 2 がもう一度使われて merged に入る。結果は [4, 2, 4, 0]。合体に使った 2 が消えずに残り、数が増えてしまうので、ゲームとして成り立たない。i++ は「使った次の要素を飛ばす」ための行。
ソースページの「書きかえて動かす」で * 2 を * 3 にすると、出てくる数が 2, 6, 18, 54 … に変わるのを確かめられます。
2048は上下左右に動かせますが、コードには「左に動かす」処理しかありません。盤面を回転させてから左に動かし、回転を戻しているからです。
// g を 90° 回転した新しい 2次元配列を返す(times 回くり返す) function rotateGrid(g, times) { var result = g; for (var t = 0; t < times; t++) { var newGrid = []; for (var r = 0; r < SIZE; r++) { newGrid[r] = []; for (var c = 0; c < SIZE; c++) { newGrid[r][c] = result[c][SIZE - 1 - r]; // (イ) } } result = newGrid; } return result; } // 方向ごとの回転回数(左を基準にする) var rotations = { left: 0, up: 1, right: 2, down: 3 }; var rot = rotations[direction]; var rotated = rotateGrid(grid, rot); // … rotated の各行に slideRow を適用 … grid = rotateGrid(rotated, (4 - rot) % 4); // (ウ) 回転を戻す
SIZE = 3 とし、g = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] とする。rotateGrid(g, 1) の結果を書きなさい。up(上に動かす)が rotations で 1 になっている理由を、(2)の答えを使って説明しなさい。(4 - rot) % 4 としている理由を説明しなさい。4 - rot だけでは何が困るか。(1) (イ)を r = 0 から順に当てはめる。newGrid[0][c] = g[c][2] → [3, 6, 9]、newGrid[1][c] = g[c][1] → [2, 5, 8]、newGrid[2][c] = g[c][0] → [1, 4, 7]。答え [[3, 6, 9], [2, 5, 8], [1, 4, 7]]。
(2) 元の右の列 [3, 6, 9] が上の行に来ているので、反時計回りに90°。
(3) 反時計回りに90°回すと、元の「上」の辺が「左」の辺に来る。だから、回転してから「左に動かす」処理をすると、元の盤面では「上に動かした」ことになる。1回の回転で上→左になるので up: 1。同様に right は2回(180°)、down は3回。
(4) 反時計回りに rot 回回したものを元に戻すには、さらに 4 - rot 回回せばよい(4回で一周するから)。ただし rot = 0 のとき 4 - rot = 4 になり、無駄に4回回す。% 4 で 0 にして、回さずに済ませている。結果は同じだが処理が無駄になる、というのが「困ること」。
「4つの方向を1つの処理にまとめる」のは、同じコードを4回書かずに済ませる関数と処理の共通化の典型例です。
迷路ゲームの迷路はプレイのたびにプログラムが作っています。次は迷路ゲームのコードの生成部分を、要点だけ残して整理したものです。
mazeVisited[y][x] はマス (x, y) をすでに通ったか、stack は配列です。
var stack = [];
mazeVisited[0][0] = true;
stack.push({ cx: 0, cy: 0 }); // スタートを積む
while (stack.length > 0) {
var current = stack[stack.length - 1]; // (エ)
// まだ通っていない、となりのマスを集める
var neighbors = [];
for (var d = 0; d < DIRS.length; d++) { // DIRS は上下左右の4方向
var nx = current.cx + DIRS[d].dx;
var ny = current.cy + DIRS[d].dy;
if (nx >= 0 && nx < MAZE_SIZE && ny >= 0 && ny < MAZE_SIZE && !mazeVisited[ny][nx]) {
neighbors.push({ nx: nx, ny: ny });
}
}
if (neighbors.length === 0) {
stack.pop(); // (オ)
} else {
var chosen = neighbors[Math.floor(Math.random() * neighbors.length)];
// current と chosen の間の壁をこわす(省略)
mazeVisited[chosen.ny][chosen.nx] = true;
stack.push({ cx: chosen.nx, cy: chosen.ny });
}
}
stack.pop() ではなく stack[stack.length - 1] と書かれている。この2つの違いを説明しなさい。pop するのは、迷路づくりのどういう場面か。日本語で説明しなさい。while ループが終わるのは、どういう状態になったときか。stack を使っている理由として考えられることを述べなさい。(1) pop() は末尾の要素を取り出して配列から消す。stack[stack.length - 1] は末尾を見るだけで消さない(peek)。ここでは「今いるマス」を見たいだけで、まだ戻るとは決めていないので消さない。
(2) となりに、まだ通っていないマスが1つもない=行き止まり。来た道を1つ戻る(バックトラック)ために、今いるマスをスタックから消す。
(3) stack が空になったとき。すべてのマスで「行き止まり→戻る」が済み、スタートまで戻りきった状態。このとき全マスが訪問済みになっている。
(4) 「どこへも行ける」:通ったマスは必ずすでに通ったマスから壁をこわしてつながっている(else 側で current と chosen の間の壁をこわす)ので、すべてのマスがスタートとつながる。「道は1本」:壁をこわすのはまだ通っていないマスに向かうときだけ(!mazeVisited の条件)なので、すでにつながっているマスどうしを二重につなぐことがなく、回り道(閉路)ができない。閉路がなければ2点間の道は1本。
(5) 再帰は「戻る場所」をコンピュータが内部のスタックに積んで管理する。迷路が大きいと呼び出しが深くなり、内部スタックの上限を超えて止まる(スタックオーバーフロー)ことがある。自分で配列を持てば深さに上限がなく、途中で止めることも簡単。実際のコードにも「スタックオーバーフロー防止」とコメントがある。
「行けるなら進む、行けなければ戻る、全部行くまでくり返す」は深さ優先探索の形。迷路を解くときにも同じ形が使えます。
ブロック崩しでは、パドルのどこに当たったかでボールの飛ぶ向きが変わります。ブロック崩しのコードから、ボールを発射するときの角度の計算です。
b.offset はパドルの左端からボールまでの距離、currentPaddleW はパドルの幅、ballSpeed は速さです。
var hitPos = Math.max(0, Math.min(1, b.offset / currentPaddleW)); // (カ) var angle = (150 - hitPos * 120) * Math.PI / 180; // (キ) b.dx = ballSpeed * Math.cos(angle); // 横向きの速さ b.dy = -Math.abs(ballSpeed * Math.sin(angle)); // (ク) 縦向きの速さ
Math.max(0, Math.min(1, …)) と二重にしているのは何のためか。b.offset がパドルの幅より大きいときの hitPos を答えなさい。hitPos が 0、0.5、1 のときの角度をそれぞれ「度」で答えなさい。また * Math.PI / 180 は何をしているか。hitPos = 0.5(パドルの真ん中)のとき、b.dx と b.dy はいくつになるか。ballSpeed = 6 として答え、ボールがどちらへ飛ぶか述べなさい。-Math.abs(…) としている理由を説明しなさい。(ヒント:画面の座標は下向きがプラス)150 と 120 を 170 と 160 に変えると、端に当てたときのボールの飛び方はどう変わるか。ゲームの手ざわりとして何が起きるか考えなさい。(1) 値を 0以上1以下に収めるため(クランプ)。ボールがパドルの端ぎりぎりに当たったときなど、計算上 b.offset が幅を超えたり負になったりしても角度が暴れないようにしている。幅より大きいとき hitPos = 1。
(2) hitPos = 0 → 150°、0.5 → 90°、1 → 30°。Math.cos・Math.sin はラジアンで角度を受け取るので、* Math.PI / 180 で度をラジアンに変換している(180° = π ラジアン)。
(3) 90° なので cos = 0、sin = 1。b.dx = 0、b.dy = -6。横には動かず、真上に飛ぶ。左端(150°)では cos が負なので左上へ、右端(30°)では右上へ飛ぶ。
(4) 画面の座標は下向きがプラスなので、ボールを上に飛ばすには dy を負にする必要がある。Math.abs で符号を必ず正にしてから - をつけることで、角度の計算がどうであれ必ず上向きになるようにしている(下に発射されて即ミス、という事故を防ぐ)。
(5) 端に当てたときの角度が 170° と 10° になり、ほとんど真横に飛ぶ。速さは同じでも縦向きの成分(sin)が小さいので、ブロックまで上がるのに時間がかかり、横の壁で何度も跳ね返る。ねらって横に飛ばせる反面、テンポが遅くなる。「数値1つでゲームの手ざわりが変わる」例。
壁での跳ね返りは dx = -dx のように符号を反転するだけ(入射角=反射角)。パドルだけ「当てた場所で角度が決まる」ルールを作者がわざと足しているのがゲームデザインです。
リズムゲームは、キーを押した時刻と「押すべき時刻」のずれで PERFECT/GREAT/GOOD/MISS を決めます。リズムゲームのコードから判定部分です。
closestDiff は、押すべき時刻とのずれ(ミリ秒)です。
var PERFECT_MS = 67, GREAT_MS = 144, GOOD_MS = 250, MISS_MS = 389; var d = Math.abs(n.targetTime - now); // (ケ) ずれの大きさ // … レーン内で d がいちばん小さいノーツを closest、その d を closestDiff とする … if (closestDiff <= PERFECT_MS) { points = 300; combo++; } else if (closestDiff <= GREAT_MS) { points = 200; combo++; } else if (closestDiff <= GOOD_MS) { points = 100; combo++; } else if (closestDiff <= MISS_MS) { points = 0; combo = 0; } else { return; // (コ) 何もしない } closest.hit = true; // (サ)
closestDiff が 100 のとき、points はいくつか。if の順番を入れかえて、closestDiff <= GOOD_MS の判定をいちばん上にすると、どんな不具合が起きるか。Math.abs を使う理由を説明しなさい。使わないと、早く押したときと遅く押したときで何が違ってしまうか。closest.hit = true がないと何が起きるか。(1) 100 は 67 より大きく 144 以下なので GREAT、200点。
(2) <= 250 を先に判定すると、ずれが 67 以下でも 144 以下でもすべて先に GOOD になってしまい、PERFECT と GREAT が二度と出ない。if … else if は上から順に、最初になりたった条件だけが実行されるので、範囲が狭い条件(厳しい条件)から並べる必要がある。
(3) 早く押すと n.targetTime - now は正、遅く押すと負になる。絶対値を取らないと、遅く押したときは常に負=どの条件にもなりたち、どんなに遅くても PERFECT になってしまう。Math.abs で「早すぎ・遅すぎを同じずれとして扱う」。
(4) ノーツがまだ遠くにあるときに押した(あるいは何もないところで押した)だけで MISS になると、リズムを取るための「空打ち」や、次のノーツに備えた早押しまで罰することになり、遊びにくい。ずれが大きすぎる入力は「そのノーツを押したつもりではない」とみなして無視している。
(5) 同じノーツが次のキー入力でも「いちばん近いノーツ」として選ばれ、1つのノーツで何度も得点できてしまう。処理済みの印(フラグ)を立てて二重判定を防いでいる。
判定を「画面上の距離(ピクセル)」ではなく時間(ミリ秒)で決めているのもポイント。ノーツの落ちる速さを変えても判定の甘さが変わらないようにするためで、リズムゲームの作り方で解説しています。
| 時 | 活動 | ポイント |
|---|---|---|
| 1 | 問1〜問3をペアで解く(配列・2次元配列・スタック) | 紙に filtered や newGrid の中身を1ステップずつ書き出すことを徹底する。答えが合っているかより、途中の値が追えているかを見る。問2は3×3で手を動かす |
| 2 | 問4・問5を解いたあと、ソースページで実物を書きかえて確かめる | 問4(5)の 150・120、問1の * 2、問5の判定幅を実際に変えて動かす。「予想→変更→確認」を1〜2行でふり返らせる |
1時間しかなければ、問1と問5(配列と分岐の順序)が扱いやすく、どちらもソースページですぐ確かめられます。 中学校 技術・家庭の段階④でも、問1・問3・問5は使えます(中学校向けのページ)。
ありません。このページの問題は、公開されているゲームの実際のJavaScriptを題材にした読解問題で、共通テストの形式(共通テスト用プログラム表記による出題)とは異なります。過去問・試作問題は大学入試センターの公開資料をご覧ください。変数の値を追う・配列の添字を追う・分岐の順序を考えるという力は共通です。
大丈夫です。問われているのは「配列の要素を順に見る」「条件で分ける」「関数に値を渡す」という言語に依存しない考え方で、共通テストも特定の言語ではなく共通テスト用プログラム表記で出題されます。JavaScriptを選んでいる理由は、生徒が遊んでいるゲームそのもののコードをブラウザだけで読み・書きかえて動かせるためです。
確かめられます。各ゲームのソースページに「このコードを書きかえて動かす」欄があり、数値を変えてその場で実行できます。たとえば問1の * 2 を * 3 にすると、出てくる数が 2, 6, 18 … に変わるのが見られます。
問1・問3・問5は中学校 技術・家庭「D 情報の技術」の段階④(コードを読む)でも使えます。問2(2次元配列の回転)と問4(三角関数)は高校向けです。
問題の追加希望や誤りのご指摘など、お気軽にお問い合わせください
ひなテック公式サイトへ →