目次を開く(全20章)
本文はオフラインでも読めます。実行ボタンを押すと、CDNから実行環境を取得します(初回は約31 MB)。Rubyの実行環境は実行ごとに新しくなり、結果の出力は16 KBまでです。
01学習レベル:基礎
Rubyが、ブラウザの中で動く
Rubyで書いた計算やゲームの規則を、Webページでも使えます。ruby.wasmは、Rubyの実行環境をWebAssembly(WASM)向けにしたもの。ブラウザがWASMを読み込み、その中でRubyのコードを実行します。
hp -= damage→ruby.wasmRubyを実行する→ブラウザの画面
HTML・Canvas
Rubyの文法はターミナルで使うものと共通です。変わるのは、入力をどこから受け取るか、結果をどこへ表示するか。この編では、まずRubyの基本を試し、そのあとでHTMLとCanvasにつなぎます。
| 担当 | 役割 |
|---|---|
| HTML | 見出し、入力欄、ボタン、Canvasを用意する。 |
| CSS | 色、大きさ、配置を整える。 |
| Ruby | 計算、分岐、データ、ゲームの規則を書く。 |
| JavaScript・WASM | Rubyを起動し、ブラウザの機能との橋渡しをする。 |
Rubyが初めてなら順番に進みます。Rubyを知っている人は第3章の導入、第13章のHTML、第14章のCanvasへ進めます。HTMLやCanvas自体の基本は、ガイドの各編へ戻って確認できます。
02学習レベル:基礎
まず、一行動かしてみる
putsは、値を文字にして表示し、改行するメソッドです。下の「実行」を押すと、本物のRuby 4.0がブラウザで動きます。最初は実行環境を読み込むため、少し待ちます。
# frozen_string_literal: true
puts "こんにちは、未來!"
puts 7 + 5
puts "Ruby #{RUBY_VERSION}"
puts RUBY_DESCRIPTION
「実行」を押すと、ここに結果が出ます。
pは、値の中身が分かる形で表示します。文字列なら引用符、配列なら角括弧も表示されます。計算した値を画面に出したいときは、puts 7 + 5のように、表示する指示も書きます。
このページのRuby実行欄では、puts・print・pの出力を「実行結果」に集めています。実行するたびに新しい環境を作るので、別の欄の変数や、前回の値は引き継ぎません。5秒を超える処理は停止します。
03学習レベル:基礎
自分のHTMLファイルで動かす
新しいフォルダーにindex.htmlを作り、次の内容を保存します。インターネットにつながった状態でブラウザで開くと、「こんにちは、Ruby …!」に変わります。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>はじめてのRuby WASM</title>
<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
<h1>Rubyをブラウザで動かす</h1>
<p id="message">Rubyを読み込んでいます…</p>
<script type="text/ruby">
require "js"
document = JS.global[:document]
message = document.querySelector("#message")
message[:textContent] = "こんにちは、Ruby #{RUBY_VERSION}!"
puts "この行は開発者ツールのコンソールに表示されます"
</script>
</body>
</html>- 実行環境を読み込む
最初のscriptはCDNからruby.wasmを読み込みます。
@ruby/4.0-wasm-wasiがRubyの言語版、@2.10.1が配布パッケージの版です。 - Rubyを書く場所を用意する
<script type="text/ruby">の中がRubyです。ブラウザだけではこの形式を実行できないため、最初の実行環境が必要です。 - 表示先を見つける
require "js"で橋渡しを読み込み、JS.global[:document]からHTMLを操作します。
自分のHTMLでは、putsは通常、開発者ツールのコンソールに出ます。ページ内へ表示するには、上の例のように要素のtextContentへ文字列を代入します。
ダブルクリックでうまく動かない場合や、外部の.rbファイル・保存・通信を試す場合は、フォルダーで次のコマンドを実行し、http://localhost:8000/を開きます。終了はターミナルでControl + Cです。
ruby -run -e httpd . -p 8000本編はRuby 4.0、配布パッケージは2.10.1で確認する構成です。latestで自動的に版を変えず、更新するときは実行スクリプトとWASM、動作例をまとめて確認します。
04学習レベル:基礎
変数・文字列・真偽値
変数は、値に名前を付けて使うためのものです。Rubyでは、最初に代入したときから使えます。文字列は引用符で囲み、ダブルクォート内の#{ }で値を埋め込めます。
# frozen_string_literal: true
name = "未來"
hp = 420
equipment = "ルビーの剣"
puts "#{name}のHPは#{hp}です"
puts "装備:#{equipment}"
hp -= 50
puts "攻撃を受けたあとのHP:#{hp}"
p hp.positive?
p nil
「実行」を押すと、ここに結果が出ます。
| 書き方 | 意味 |
|---|---|
42 / 3.14 | 整数 / 小数 |
"未來" | 文字列(String) |
:hp | シンボル。ハッシュのキーなどに使う名札。 |
true / false | 真 / 偽 |
nil | 値がないことを表す。 |
Rubyで偽として扱う値はfalseとnilだけです。0、""、[]は真です。HPが0か調べるときはhp.zero?やhp == 0と書きます。
# frozen_string_literal: trueは、コード中に直接書いた文字列を変更できなくする指定です。変数への再代入はできます。文字列を<<で組み立てるときはtext = +""で変更できる文字列を作ります。
05学習レベル:基礎
計算と型変換
整数どうしの割り算は、結果も整数です。割合を計算するときは、小数に変換するかfdivを使います。入力欄やgetsから受け取る値は文字列なので、計算の前に数へ変換します。
# frozen_string_literal: true
puts 7 / 2
puts 7.fdiv(2)
puts 7 % 2
puts 2 ** 10
hp = 315
max_hp = 420
puts "HPの割合:#{(hp.fdiv(max_hp) * 100).round}%"
p "42abc".to_i
p Integer("42abc", exception: false)
p Integer("42", exception: false)
p Math.sqrt(16)
「実行」を押すと、ここに結果が出ます。
to_iは読めるところまで変換します。Integer(..., exception: false)は全体を整数として読めなければnilです。| 書き方 | 意味 |
|---|---|
+ - * / % ** | 加算・減算・乗算・除算・剰余・累乗 |
n.clamp(0, 420) | 値を0〜420の範囲へ収める。 |
[hp - damage, 0].max | 小さいほうの限界を0にする。 |
format("%.2f", value) | 小数点以下2桁の文字列にする。 |
0.1r + 0.2r | 有理数のまま計算する。 |
Floatは2進で近似した値なので、0.1 + 0.2はぴったり0.3になりません。金額なら円単位の整数にするなど、表す値に合う型を選びます。
06学習レベル:基礎
条件で行動を分ける
ifは、条件によって実行する処理を分けます。elsifで次の条件、elseでどれにも当てはまらない場合を書き、endで閉じます。
# frozen_string_literal: true
hp = 80
mp = 25
if hp <= 100 && mp >= 20
puts "叡智の祈りで回復する"
elsif mp >= 30
puts "白光の一閃を使う"
else
puts "ルビーの剣で攻撃する"
end
command = :guard
case command
when :attack then puts "攻撃!"
when :guard then puts "守りを固める!"
else puts "行動を選んでください"
end
「実行」を押すと、ここに結果が出ます。
| 書き方 | 意味 |
|---|---|
== != < > <= >= | 等しい / 等しくない / 大小の比較 |
&& / || / ! | かつ / または / ではない |
puts "勝利!" if enemy_hp.zero? | 短い条件は行の後ろにも書ける。 |
return unless ready | readyが偽なら、そのメソッドから戻る。 |
07学習レベル:基礎
繰り返しとブロック
同じ処理を決まった回数行うならtimes、集まりの値を一つずつ使うならeachです。do ... endで囲む処理のかたまりをブロックと呼びます。
# frozen_string_literal: true
3.times do |index|
puts "#{index + 1}回目の攻撃"
end
%w[紅玉の鎧 叡智の冠 コードの書].each do |item|
puts "#{item}を装備した"
end
enemy_hp = 90
while enemy_hp > 0
enemy_hp = [enemy_hp - 30, 0].max
puts "大魔王のHP:#{enemy_hp}"
end
「実行」を押すと、ここに結果が出ます。
breakは繰り返しを終え、nextは次の周回へ進みます。whileの条件が変わることを確認しましょう。このページの実行欄はWorkerで動かすので、「停止」ボタンで止められます。
[1, 2, 3].map { it * 2 }のitは最初のブロック引数を表します。まずは|number|のように名前を付けると、値の役割を追いやすくなります。二つの引数が必要なら|value, index|と書きます。
08学習レベル:基礎
配列と2次元の地図
配列は、値を順番に並べたものです。番号は0から数えます。地図は「行の配列」をさらに配列へ入れると、map[row][column]で1マスを取り出せます。
# frozen_string_literal: true
items = ["薬草", "ルビーの剣", "コードの書"]
puts items[0]
puts items[-1]
items << "紅玉の鎧"
p items.length
map = [
["草地", "森", "城"],
["道", "道", "山"],
["村", "草地", "海"]
]
puts "右上のマス:#{map[0][2]}"
board = Array.new(2) { Array.new(3, 0) }
board[0][0] = 1
p board
「実行」を押すと、ここに結果が出ます。
| 書き方 | 意味 |
|---|---|
a << value / a.pop | 末尾へ追加 / 末尾から取り出す。 |
a.each_with_index { |value, i| ... } | 値と番号を一緒に取り出す。 |
a.map { |x| x * 2 } | 各値を変換した新しい配列を作る。 |
a.select { |x| x > 0 } | 条件に合う値だけ集める。 |
a.sum / a.sample | 合計 / ランダムに一つ選ぶ。 |
Array.new(2, Array.new(3, 0))では、2行が同じ配列を参照します。片方を変えるともう片方も変わるため、地図にはArray.new(2) { Array.new(3, 0) }と書きます。
09学習レベル:基礎
ハッシュ・集合・値のまとまり
ハッシュは、番号の代わりにキーで値を取り出します。勇者の名前やHPを、ひとまとまりにできます。Setは重複を持たない集まり、Dataは少数の値を名前付きでまとめる型です。
# frozen_string_literal: true
hero = { name: "未來", hp: 420, mp: 100 }
hero[:hp] -= 50
puts "#{hero[:name]}のHP:#{hero.fetch(:hp)}"
puts hero.fetch(:weapon, "素手")
visited = Set["村", "森", "村"]
p visited.to_a
p visited.include?("城")
Point = Data.define(:x, :y)
position = Point.new(x: 2, y: 3)
moved = position.with(x: 4)
p position
p moved
「実行」を押すと、ここに結果が出ます。
hero[:gold]は、キーがなければnilです。必ずあるべきキーはfetch(:gold)で取り出すと、抜けていることをエラーで発見できます。Hash.new([])も同じ配列を共有するため、キーごとの配列にはブロック付きのHash.new { |h, key| h[key] = [] }を使います。
10学習レベル:基礎
メソッドとラムダ
処理に名前を付けると、同じ規則を何度も使えます。メソッドはdefで定義し、最後に評価した式の値を返します。処理を値として渡したいときは、ラムダが使えます。
# frozen_string_literal: true
def damage_for(attack:, defense:)
[attack - defense, 1].max
end
def remaining_hp(hp, damage)
[hp - damage, 0].max
end
damage = damage_for(attack: 125, defense: 30)
puts "ダメージ:#{damage}"
puts "残りHP:#{remaining_hp(80, damage)}"
double = ->(number) { number * 2 }
puts double.call(12)
p [1, 2, 3].map { |number| double.call(number) }
「実行」を押すと、ここに結果が出ます。
| 書き方 | 意味 |
|---|---|
def greet(name = "勇者") | 引数を省略したときの値を指定する。 |
def hit(power:, defense:) | 役割が分かるキーワード引数を受け取る。 |
def run(&block) | ブロックを受け取り、block.callで呼ぶ。 |
def square(x) = x * x | 一つの式だけのメソッドを短く書く。 |
empty? / sort! | ?は問い、!は同名のメソッドより注意が必要な版という慣習。 |
後半で使うaddEventListenerは、クリックされたときに実行するブロックを受け取ります。「いま実行する処理」と「あとで呼ばれる処理」を分けて考えましょう。
11学習レベル:基礎
クラスに状態と操作をまとめる
勇者や敵がそれぞれHPを持つなら、クラスで共通の形を作れます。initializeはnewで作るときに呼ばれ、@hpはその個体が持つ値です。
# frozen_string_literal: true
class Fighter
attr_reader :name, :hp
def initialize(name, hp)
@name = name
@hp = hp
end
def take_damage(amount)
@hp = [@hp - amount, 0].max
end
def alive?
@hp.positive?
end
end
hero = Fighter.new("未來", 420)
boss = Fighter.new("大魔王", 960)
boss.take_damage(230)
puts "#{hero.name}のHP:#{hero.hp}"
puts "#{boss.name}のHP:#{boss.hp}"
p boss.alive?
「実行」を押すと、ここに結果が出ます。
| 書き方 | 意味 |
|---|---|
attr_reader :hp | 外から読むメソッドを作る。 |
attr_accessor :name | 読む・書くメソッドを作る。 |
private | 以降のメソッドを内部から使うものにする。 |
class Hero < Fighter | 共通の性質を継承する。 |
module Heal; ...; end / include Heal | 複数のクラスで使う機能をまとめ、取り込む。 |
HPを自由に書き換えられるようにするより、take_damageやhealへ変更を集めると、「HPを0未満にしない」という規則を守りやすくなります。
12学習レベル:基礎
入力を受け取り、エラーを確かめる
ターミナルのgetsは標準入力から読みます。このガイドの実行欄では、下の入力欄を一行ずつ読めるようにしています。自分で作るWebページの入力欄は、第13章のようにDOMから値を受け取ります。
# frozen_string_literal: true
text = $stdin.gets.to_s.chomp
number = Integer(text, exception: false)
if number.nil?
puts "整数を入力してください"
else
puts "回復量:#{number.clamp(0, 140)}"
end
begin
puts 10 / 0
rescue ZeroDivisionError => error
puts "計算できません:#{error.message}"
end
「実行」を押すと、ここに結果が出ます。
SyntaxErrorなら文法、NameErrorなら名前、NoMethodErrorならその値に使えないメソッドを疑います。rescueは予想できる失敗を扱う場所です。原因の分からない失敗を全部隠すと、間違いを見つけにくくなります。
13学習レベル:実践
RubyからHTMLとボタンを操作する
require "js"で、Rubyからブラウザの機能を呼べます。querySelectorで要素を見つけ、[:value]で入力を読み、[:textContent]で文章を表示します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rubyで勇者の名前を表示する</title>
<style>
body { margin: 0; padding: 24px; color: #14202b; background: #f6f5f2; font-family: sans-serif; }
h1 { font-size: 20px; }
input, button { max-width: 100%; padding: 10px; font: inherit; }
button { margin: 8px 0; background: #1b4e77; color: white; border: 0; border-radius: 6px; }
#message { line-height: 1.8; }
</style>
<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
<h1>冒険のはじまり</h1>
<label>勇者の名前 <input id="name" value="未來" maxlength="30"></label>
<button id="greet" type="button" disabled>冒険に出る</button>
<p id="message" role="status">Rubyを読み込んでいます…</p>
<script type="text/ruby">
require "js"
document = JS.global[:document]
input = document.querySelector("#name")
button = document.querySelector("#greet")
message = document.querySelector("#message")
button.addEventListener("click") do |_event|
name = input[:value].to_s.strip
message[:textContent] = if name.empty?
"名前を入力してください。"
else
"#{name}はルビーの剣を手に、冒険に出た!"
end
end
button[:disabled] = false
message[:textContent] = "準備できました。名前を入力してボタンを押しましょう。"
</script>
</body>
</html>「作例を起動」でRubyを読み込みます。コードを変えるときはHTMLを保存して編集します。
addEventListener("click") do ... endの中は、ボタンが押されたときに実行されます。名前を読み込む処理もブロックの中にあるので、入力し直した名前が使われます。Rubyの準備ができるまでは、ボタンを無効にしています。
| 書き方 | 意味 |
|---|---|
JS.global[:document] | ブラウザのdocumentを取り出す。 |
document.querySelector("#name") | idがnameの要素を見つける。 |
input[:value].to_s | JavaScript側の文字列をRubyの文字列にする。 |
element[:textContent] = text | HTMLとして解釈せず、文字として表示する。 |
button[:disabled] = false | JavaScript側のプロパティへRubyの値を渡す。 |
ブラウザから返った値はJS::Objectです。文字列ならto_s、整数ならto_i、小数ならto_fへ変換します。JavaScriptのfalseをそのままRubyのifへ渡すと、オブジェクトなので真になります。真偽はvalue == JS::Trueで比較するか、対応する真偽変換メソッドを使います。
14学習レベル:実践
RubyからCanvasに描く
Canvasの描画命令も、Rubyから呼べます。描画先を見つけてgetContext("2d")を呼ぶところは、Canvas編と同じです。色などのプロパティには[:fillStyle] = ...の形で代入します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RubyでCanvasに描く</title>
<style>
body { margin: 0; padding: 20px; color: #14202b; background: #f6f5f2; font-family: sans-serif; }
h1 { font-size: 20px; }
canvas { display: block; width: 100%; max-width: 480px; height: auto; border-radius: 8px; }
button { margin-top: 12px; padding: 10px 16px; color: white; background: #1b4e77; border: 0; border-radius: 6px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
<h1>ルビーの剣の白い光</h1>
<canvas id="stage" width="480" height="230" aria-label="左に勇者の未來、右に大魔王。ボタンで白い光を描く戦闘の図。">未來と大魔王の戦闘シーン</canvas>
<button id="attack" type="button" disabled>白光の一閃</button>
<p id="status" role="status">Rubyを読み込んでいます…</p>
<script type="text/ruby">
require "js"
document = JS.global[:document]
canvas = document.querySelector("#stage")
ctx = canvas.getContext("2d")
button = document.querySelector("#attack")
status = document.querySelector("#status")
draw = ->(light) do
ctx.clearRect(0, 0, 480, 230)
ctx[:fillStyle] = "#182238"
ctx.fillRect(0, 0, 480, 230)
ctx[:fillStyle] = "#bd4765"
ctx.fillRect(65, 90, 50, 80)
ctx[:fillStyle] = "#dabd81"
ctx.fillRect(70, 72, 40, 18)
ctx[:fillStyle] = "#805397"
ctx.fillRect(345, 65, 70, 105)
ctx[:fillStyle] = "#ffffff"
ctx[:font] = "16px sans-serif"
ctx.fillText("未來", 70, 200)
ctx.fillText("大魔王", 350, 200)
if light
ctx[:strokeStyle] = "#fff5d7"
ctx[:lineWidth] = 12
ctx.beginPath
ctx.moveTo(120, 112)
ctx.lineTo(365, 90)
ctx.stroke
end
end
draw.call(false)
button.addEventListener("click") do |_event|
draw.call(true)
status[:textContent] = "ルビーの剣から、白い光が放たれた!"
end
button[:disabled] = false
status[:textContent] = "準備できました。必殺技を使ってみましょう。"
</script>
</body>
</html>「作例を起動」でRubyを読み込みます。コードを変えるときはHTMLを保存して編集します。
座標の原点は左上です。右へ進むとxが増え、下へ進むとyが増えます。ここではキャラクターを四角形で表し、「背景→勇者と敵→文字→白い光」の順に描いています。あとから描いたものが手前に重なります。
| 書き方 | 意味 |
|---|---|
ctx.fillRect(x, y, w, h) | 四角形を塗る。 |
ctx.beginPath | 新しい線を描き始める。 |
ctx.moveTo(x, y) / ctx.lineTo(x, y) | 始点へ移る / 次の点まで線をつなぐ。 |
ctx.stroke / ctx.fill | 線を引く / 囲んだ領域を塗る。 |
ctx.save / ctx.restore | 色や変形などの描画状態を保存 / 復元する。 |
詳しい描画はCanvas編の最初の3行、高精細ディスプレイへの対応を参照してください。JavaScriptで覚えた描画の順序や座標は、そのまま使えます。
15学習レベル:発展
アニメーションは、次の一コマを予約する
無限ループで画面を描き続けると、クリックや表示の更新が進みません。requestAnimationFrameへ処理を渡し、一コマを描いたらブラウザへ制御を返します。
前章のCanvasを用意したHTMLで、Rubyの描画処理を次のように置き換えます。ブラウザが渡す時刻はJavaScriptの数値なので、to_fで変換します。
require "js"
canvas = JS.global[:document].querySelector("#stage")
ctx = canvas.getContext("2d")
x = 20.0
last = nil
frame = nil
frame = proc do |timestamp|
now = timestamp.to_f
dt = last ? [(now - last) / 1000.0, 0.05].min : 0.0
last = now
x = (x + 100 * dt) % 480
ctx.clearRect(0, 0, 480, 230)
ctx[:fillStyle] = "#bd4765"
ctx.fillRect(x, 95, 30, 30)
JS.global.requestAnimationFrame(frame)
end
JS.global.requestAnimationFrame(frame)x += 100 * dtは、1秒あたり100進むという意味です。画面の更新回数が違っても速さがそろいます。タブを戻した直後に飛ばないよう、dtを最大0.05秒に抑えています。停止する部品を作るなら、予約時に返る番号を保持してcancelAnimationFrameへ渡します。
考え方はCanvas編のアニメーションと共通です。Canvasは毎回描き直し、HPや位置などの状態はRubyの変数やオブジェクトへ保持します。
16学習レベル:実践
JSONとlocalStorageで冒険を保存する
ブラウザを閉じても残したい値は、保存する指示が必要です。小さなゲームの状態なら、JSONの文字列にしてlocalStorageへ入れる方法があります。公開するページと同じように、HTTPのURLで試しましょう。
# frozen_string_literal: true
require "json"
hero = { name: "未來", hp: 315, mp: 70 }
json = JSON.generate(hero)
puts json
saved = JSON.parse(json)
puts "#{saved.fetch("name")}のHP:#{saved.fetch("hp")}"
「実行」を押すと、ここに結果が出ます。
require "js"
require "json"
storage = JS.global[:localStorage]
hero = { name: "未來", hp: 315, mp: 70 }
storage.setItem("mirai-adventure-v1", JSON.generate(hero))
raw = storage.getItem("mirai-adventure-v1")
unless raw == JS::Null
begin
saved = JSON.parse(raw.to_s)
hp = saved.fetch("hp")
raise "HPの形式が違います" unless hp.is_a?(Integer)
hero[:hp] = hp.clamp(0, 420)
rescue JSON::ParserError, KeyError, RuntimeError => error
warn "保存データを読めません:#{error.message}"
end
end容量の上限やブラウザの設定によって、localStorageの取得・書き込みが失敗することがあります。実際のゲームでは保存操作の失敗を受け止め、画面へ知らせます。file://での保存にはブラウザごとの差があるため、ローカルサーバーや公開先で確認します。このガイド内の隔離した作例には保存しません。
17学習レベル:発展
外部ファイルと非同期処理
RubyのFile.readが読むのは、WASM側へ用意されたファイルです。Webサーバー上のJSONやYAMLを読みたいときは、ブラウザのfetchを経由します。通信の終了を待つときは、非同期実行の指定が必要です。
<script type="text/ruby" data-eval="async">
require "js"
require "json"
response = JS.global.fetch("./hero.json").await
raise "取得に失敗しました" unless response[:ok] == JS::True
text = response.text.await.to_s
hero = JSON.parse(text)
JS.global[:document].querySelector("#message")[:textContent] =
"#{hero.fetch("name")}のHP:#{hero.fetch("hp")}"
</script>{"name":"未來","hp":420,"mp":100}awaitは通信が終わるまでそのRubyの続きを待ちます。data-eval="async"を付けることで、この待ち方を使えます。JavaScriptからVMを直接操作する場合は、vm.evalAsync(...)を使います。
標準ライブラリ込みの環境でrequire "yaml"を使い、取得した文字列をYAML.safe_load(text, permitted_classes: [], permitted_symbols: [], aliases: false)へ渡します。敵のHPや装備名など、読み込んだ項目と型も確認します。
fetch("./hero.json")はfile://では制限されます。第3章のローカルサーバーで試してください。別のサイトから取得する場合は、相手側のCORSの許可も必要です。通信の例ではネットワーク切断やHTTPエラーを画面に伝える処理を加えて完成させます。
18学習レベル:実践
ターミナル版との違いを知る
Rubyの文法が同じでも、使えるOSの機能やライブラリは実行環境によって変わります。付録のコードを移すときは、「文法」と「外部とのやりとり」を分けて確認します。
| ターミナルでの書き方 | ブラウザで確認すること |
|---|---|
| 変数・分岐・配列・メソッド・クラス | 本編の基本例として使える。 |
puts / p | 標準では開発者ツールへ。このガイドは出力欄へ集める。 |
gets / io/console | Webページの入力はDOM・イベントで受け取る。このガイドのgetsは用意した文字列を読む。 |
File.read / File.write | WASMへ渡した仮想ファイルの操作。利用者のMacのファイルを直接読むものではない。 |
require "json" / require "yaml" | 標準ライブラリを含む配布環境を選び、必要なものが入っているか確かめる。 |
Thread / Ractor | 通常のRubyと同じ並列処理を前提にしない。現行WASI版のThread関連APIには制限がある。 |
Net::HTTP / ネイティブ拡張gem | 通常のOS版のまま使えるとは限らない。通信はブラウザのfetch、gemはWASMへの対応を確認する。 |
system / シェルコマンド | 利用者のPCのコマンドを起動するものとして扱わない。 |
ruby.wasmには標準拡張を省いたminimalと、含めたfullがあります。本編の実行欄はruby+stdlib.wasmを使います。ライブラリの有無を、Rubyの言語版だけで判断しないようにしましょう。
付録にあるスレッド、外部プロセス、バイナリファイルなどは、環境を確認してから使う発展項目です。まずはHP、条件分岐、配列、メソッドといった、外の機能に依存しない規則をRubyへまとめると移しやすくなります。
19学習レベル:基礎
Ruby文法の早見表
書き方を忘れたら、ここから戻れます。元の付録にある項目から、ブラウザの小さな作品に使いやすいものをまとめました。文法と標準クラスの詳しい説明はRuby 4.0の公式リファレンスへ進んでください。
| 用途 | 書き方 |
|---|---|
| 変数・定数 | hp = 420 / MAX_HP = 420 |
| 文字列 | "#{name}のHP" / s.length / s.strip / s.split(",") |
| 型変換 | to_s / to_i / Integer(text, exception: false) |
| 条件 | if ... elsif ... else ... end / case ... when ... end |
| 繰り返し | 3.times { |i| ... } / each do |x| ... end |
| 配列 | [1, 2, 3] / a[0] / a << 4 / a.pop |
| ハッシュ | { hp: 420 } / h[:hp] / h.fetch(:hp) |
| 集計 | map / select / find / sum / tally |
| メソッド | def hit(power:) ... end / def square(x) = x * x |
| ラムダ・ブロック | ->(x) { x * 2 } / call / yield |
| クラス・モジュール | class / initialize / attr_reader / include |
| 値の型 | Data.define(:x, :y) / Struct.new(:x, :y) |
| パターンマッチ | case value; in { hp: Integer => hp }; ...; end |
| 正規表現 | /\A[0-9]+\z/.match?(text) |
| 例外 | begin ... rescue ErrorClass => error ... end |
| 調べる | p value / value.class / value.respond_to?(:name) |
| 乱数・範囲 | rand(10..20) / (1..5).each |
| ビット演算 | & / | / ^ / << / >> |
| 時刻 | Time.now / Time.now.strftime("%H:%M") |
| Ruby→ブラウザ | require "js" / JS.global / object[:property] |
20学習レベル:基礎
つまずいたときと、次の一歩
小さな成功を一つずつ確かめながら進めます。「Rubyが動く」「ボタンで値が変わる」「Canvasに描ける」を別々に確かめると、うまくいかない場所を絞れます。
| 困ったこと | まず確認すること |
|---|---|
| いつまでも読み込み中 | 通信、CDNへの接続、開発者ツールのNetworkを確認する。初回は約31 MBの取得がある。 |
| putsがページに出ない | 自分のHTMLでは開発者ツールのConsoleを確認する。ページへの表示はtextContentを使う。 |
| 名前の間違いで止まる | エラーの行番号、綴り、変数を作った場所を確認する。 |
| ボタンを押しても変わらない | Rubyが起動済みか、idとquerySelectorの指定が一致するかを確認する。 |
| JavaScriptのfalseでifに入る | JS::Objectを真偽値へ変換するか、JS::Trueとの比較を使う。 |
| 処理が終わらない | 実行欄の停止を押す。whileの条件や、状態の更新を確認する。 |
| 通信・保存だけ動かない | file://ではなくhttp://localhostで試し、通信や保存の例外を確認する。 |
| Rubyを再実行すると値が消える | このページは実行ごとに新しいVM。自分のアプリでは状態と保存の責任を決める。 |
第11章のFighterを使い、「攻撃ボタンで敵のHPを減らす」「0になったら勝利を表示する」「回復を加える」の順に作ってみましょう。規則をRuby、ステータスとボタンをHTML、戦闘の絵をCanvasへ分けると、役割が見えやすくなります。
公式資料
- ruby.wasm公式README:導入、配布形態、WASI版の制限。
- 公式Cheat Sheet:ブラウザでの起動と非同期実行。
- 公式FAQ:putsの出力先。
- JSモジュール・JS::Object:Rubyとブラウザの橋渡し。
- Ruby 4.0日本語リファレンス:文法・標準クラス。
文法の構成は、書籍付録「Ruby 文法クイックリファレンス」を参考にしました。最終確認時の対象はRuby 4.0 / ruby.wasm 2.10.1です。
Canvas編で、描く仕組みを学ぶ四角形から、線・画像・アニメーション・操作へ。Rubyから呼ぶ描画APIも共通です。