Skip to content

Instantly share code, notes, and snippets.

@machida
Created April 30, 2026 17:35
Show Gist options
  • Select an option

  • Save machida/8780dd74676b7b4e483cb34137a85930 to your computer and use it in GitHub Desktop.

Select an option

Save machida/8780dd74676b7b4e483cb34137a85930 to your computer and use it in GitHub Desktop.
Claude Codeとのやりとり
❯ ブラウザで遊べるブラックジャックゲームを作成してください。
スピネル( https://github.com/matz/spinel )とWASMを使ってください。
⏺ Explore(Research Spinel language and WASM usage)
⎿  Done (16 tool uses · 33.8k tokens · 3m 50s)
(ctrl+o to expand)
⏺ Spinelのリポジトリを直接確認します。
⏺ Bash(gh repo view matz/spinel --json description,readme,topics 2>/dev/null |
head -100)
⎿  (No output)
⏺ Bash(gh api repos/matz/spinel/contents/README.md --jq '.content' 2>/dev/null |
base64 -d 2>/dev/null | head -150)
⎿  # Spinel -- Ruby AOT Compiler
Spinel compiles Ruby source code into standalone native executables.
… +57 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents --jq '.[].name' 2>/dev/null)
⎿  .github
.gitignore
AUTHORS
… +10 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/lib --jq '.[].name' 2>/dev/null)
⎿  erb.rb
forwardable.rb
mruby_shim.h
… +10 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/lib/sp_runtime.h --jq '.content'
2>/dev/null | base64 -d 2>/dev/null | head -80)
⎿  /* Spinel Runtime Library */
#ifndef SP_RUNTIME_H
#define SP_RUNTIME_H
… +32 lines (ctrl+o to expand)
⏺ Spinelの状況を確認しました。SpinelはRuby→C→ネイティブバイナリのコンパイラでWAS
Mを直接サポートしていませんが、Spinel で Ruby→C に変換し、EmscriptenでそのCをW
ASMにコンパイルする方法が実現可能です。まず環境を確認します。
⏺ Bash(which emcc 2>/dev/null && emcc --version 2>/dev/null | head -3; which
ruby 2>/dev/null && ruby --version; which make 2>/dev/null; ls
/Users/machida/dev/bj/ 2>/d…)
⎿  emcc not found
/Users/machida/.rbenv/shims/ruby
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
/usr/bin/make
Listed 1 directory (ctrl+o to expand)
⏺ Bash(brew list emscripten 2>/dev/null && emcc --version 2>/dev/null | head -2;
ls $(brew --prefix emscripten 2>/dev/null)/bin/emcc 2>/dev/null)
⎿  Error: Exit code 1
⏺ Bash(brew search emscripten 2>/dev/null | head -5; which wasm-pack
2>/dev/null; which wabt 2>/dev/null)
⎿  Error: Exit code 1
emscripten
enscript
descript
wasm-pack not found
wabt not found
⏺ SpinelはWASMを直接サポートしていませんが、Ruby→Spinel→C→Emscripten→WASMという
パイプラインで実現できます。まず依存関係のインストールとSpinelのビルドを確認し
ます。
⏺ Bash(gh api repos/matz/spinel/contents/benchmark --jq '.[].name' 2>/dev/null |
head -20)
⎿  attr_accessor_bench.rb
bm_ackermann.rb
bm_ao_render.rb
… +17 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null | head
-20)
⎿  anon_block_forward.rb
array2.rb
array3.rb
… +17 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/benchmark/bm_fasta.rb --jq '.content'
2>/dev/null | base64 -d 2>/dev/null)
⎿  # The Computer Language Benchmarks Game - Fasta
# Generate random DNA sequences
… +46 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test/array_push.rb --jq '.content'
2>/dev/null | base64 -d 2>/dev/null)
⎿  # Array#push across typed-array variants.
# Previously push only dispatched for IntArray and StrArray; FloatArra
y
… +32 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null |
xargs -I{} sh -c 'gh api repos/matz/spinel/contents/test/{} --jq
".content" 2>/dev/null | b…)
⎿  Running…
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null >
/tmp/spinel_tests.txt; cat /tmp/spinel_tests.txt | wc -l)
⎿  Waiting in the background (↓ to manage)
⏺ Seasoning… (5m 58s · ↓ 16.2k tokens)
─────201 ───────────────────────────────────────────────────────────────────
⏺ Bash(for test in string_split string_scan string_gsub io_read; do
result=$(gh api repos/matz/spinel/contents/test/${test}.rbg--jq/effort
'.content' 2>/dev/null | base64 -…)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null |
xargs -I{} sh -c 'gh api repos/matz/spinel/contents/test/{} --jq
".content" 2>/dev/null | b…)
⎿  Running in the background (↓ to manage)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null >
/tmp/spinel_tests.txt; cat /tmp/spinel_tests.txt | wc -l)
⎿  201
⏺ Bash(for test in string_split string_scan string_gsub io_read; do
result=$(gh api repos/matz/spinel/contents/test/${test}.rb --jq
'.content' 2>/dev/null | base64 -…)
⎿  (No output)
⏺ Bash(gh api repos/matz/spinel/contents/benchmark/io_wordcount.rb --jq
'.content' 2>/dev/null | base64 -d 2>/dev/null)
⎿  # Line-by-line file processing with File.open block
# Tests: File.open, each_line, string split, Hash
… +38 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null | grep
-E "rand|gets|string" | head -10)
⎿  empty_hash_ivar_string_value.rb
env_string_inference.rb
module_cls_method_string_return.rb
… +6 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/lib/sp_runtime.h --jq '.content'
2>/dev/null | base64 -d 2>/dev/null | grep -A5 -B2
"rand\|gets\|getline\|fgets" | head -50)
⎿  static sp_IntArray*sp_IntArray_sort(sp_IntArray*a){sp_IntArray*b=sp_In
tArray_dup(a);qsort(b->data+b->start,b->len,sizeof(mrb_int),_sp_int_cm
p);return b;}
… +105 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/lib/sp_runtime.h --jq '.content'
2>/dev/null | base64 -d 2>/dev/null | grep -E
"pop|shift|delete|last|first|sample" | head -20)
⎿  typedef struct{mrb_int first;mrb_int last;}sp_Range;
static sp_Range sp_range_new(mrb_int f,mrb_int l){sp_Range r;r.first=f
;r.last=l;return r;}
… +70 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test --jq '.[].name' 2>/dev/null | grep
-E "range|string" | head -10)
⎿  array_range.rb
empty_hash_ivar_string_value.rb
env_string_inference.rb
… +7 lines (ctrl+o to expand)
⏺ Background command "gh api repos/matz/spinel/contents/test --jq '.[].name'
2>/dev/null | xargs -I{} sh -c 'gh api repos/matz/spinel/contents/test/{}
--jq ".content" 2>/dev/null | base64 -d 2>/dev/null | grep -l
"gets\|stdin\|split\|rand" 2>/dev/null && echo "file: {}"'
2>/dev/null | head -30" completed (exit code 0)
⏺ Bash(gh api repos/matz/spinel/contents/benchmark --jq '.[].name' 2>/dev/null |
grep -v "^bm_" | head -20)
⎿  attr_accessor_bench.rb
csv_process.rb
io_wordcount.rb
… +8 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test/strings.rb --jq '.content'
2>/dev/null | base64 -d 2>/dev/null | head -60)
⎿  # Test Symbol and String methods
# Symbol
… +20 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test/range.rb --jq '.content'
2>/dev/null | base64 -d 2>/dev/null)
⎿  r = (1..10)
puts r.first # 1
puts r.last # 10
… +13 lines (ctrl+o to expand)
⏺ Bash(gh api repos/matz/spinel/contents/test/strings2.rb --jq '.content'
2>/dev/null | base64 -d 2>/dev/null | head -60)
⎿  # Test additional String methods
str = " Hello, World! "
… +20 lines (ctrl+o to expand)
⏺ Spinelはサポートしている機能を把握しました。Ruby → Spinel → C → Emscripten →
WASMのパイプラインで実装します。
設計方針:
- Ruby(Spinel対応)でゲームロジックを書き、stdioでコマンドを受け渡し
- 各ボタン操作ごとに新しいWASMインスタンスを作成(シンプルで確実)
まずディレクトリ構造と全ファイルを作成します。
⏺ Bash(mkdir -p /Users/machida/dev/bj/{src,web,generated} && ls
/Users/machida/dev/bj/)
⎿  generated/
src/
web/
⏺ Write(src/blackjack.rb)
────────────────────────────────────────────────────────────────────────────────
Create file
src/blackjack.rb
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
1 # Blackjack game for Spinel AOT compiler + WASM
2 # Protocol (stdin): command|chips|bet|seed|player_cards|dealer_cards|de
ck
3 # Protocol (stdout): phase|result|chips|bet|message|player_cards|dealer
_cards|deck
4 # Cards encoded as 0-51: suit=card/13 (S/H/D/C), value=card%13 (A/2/../
K)
5
6 SUIT_CHARS = ["S", "H", "D", "C"]
7 VAL_STRS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "
Q", "K"]
8
9 $rng = 1
10
11 def rng_next(max)
12 $rng = ($rng * 1664525 + 1013904223) % 2147483647
13 $rng % max
14 end
15
16 def card_str(card)
17 VAL_STRS[card % 13] + SUIT_CHARS[card / 13]
18 end
19
20 def card_points(card)
21 v = card % 13
22 if v == 0
23 11
24 elsif v >= 10
25 10
26 else
27 v + 1
28 end
29 end
30
31 def hand_score(hand)
32 total = 0
33 aces = 0
34 i = 0
35 while i < hand.length
36 p = card_points(hand[i])
37 if p == 11
38 aces += 1
39 end
40 total += p
41 i += 1
42 end
43 while total > 21 && aces > 0
44 total -= 10
45 aces -= 1
46 end
47 total
48 end
49
50 def encode_cards(cards)
51 result = ""
52 i = 0
53 while i < cards.length
54 if i > 0
55 result = result + ","
56 end
57 result = result + cards[i].to_s
58 i += 1
59 end
60 result
61 end
62
63 def decode_cards(str)
64 cards = []
65 if str.length == 0
66 return cards
67 end
68 parts = str.split(",")
69 i = 0
70 while i < parts.length
71 if parts[i].length > 0
72 cards << Integer(parts[i])
73 end
74 i += 1
75 end
76 cards
77 end
78
79 def do_shuffle(deck)
80 n = deck.length
81 i = n - 1
82 while i > 0
83 j = rng_next(i + 1)
84 tmp = deck[i]
85 deck[i] = deck[j]
86 deck[j] = tmp
87 i -= 1
88 end
89 end
90
91 def fresh_deck
92 deck = []
93 i = 0
94 while i < 52
95 deck << i
96 i += 1
97 end
98 deck
99 end
100
101 def dealer_draw(dealer_hand, deck)
102 while hand_score(dealer_hand) < 17
103 dealer_hand << deck.pop
104 end
105 end
106
107 def resolve_winner(ps, ds, chips, bet)
108 if ds > 21 || ps > ds
109 ["win", chips + bet, "You win!"]
110 elsif ps == ds
111 ["push", chips, "Push — bet returned."]
112 else
113 ["lose", chips - bet, "Dealer wins."]
114 end
115 end
116
117 # --- read input ---
118 line = gets.chomp
119 parts = line.split("|")
120
121 cmd = parts[0]
122 chips = 1000
123 bet = 100
124 seed = 1
125
126 if parts.length > 1 && parts[1].length > 0
127 chips = Integer(parts[1])
128 end
129 if parts.length > 2 && parts[2].length > 0
130 bet = Integer(parts[2])
131 end
132 if parts.length > 3 && parts[3].length > 0
133 seed = Integer(parts[3])
134 end
135 $rng = seed
136
137 player_hand = []
138 dealer_hand = []
139 deck = []
140
141 if parts.length > 4 && parts[4].length > 0
142 player_hand = decode_cards(parts[4])
143 end
144 if parts.length > 5 && parts[5].length > 0
145 dealer_hand = decode_cards(parts[5])
146 end
147 if parts.length > 6 && parts[6].length > 0
148 deck = decode_cards(parts[6])
149 end
150
151 phase = "playing"
152 result = ""
153 message = ""
154
155 if cmd == "new"
156 deck = fresh_deck
157 do_shuffle(deck)
158
159 c1 = deck.pop
160 c2 = deck.pop
161 c3 = deck.pop
162 c4 = deck.pop
163 player_hand = []
164 player_hand << c1
165 player_hand << c3
166 dealer_hand = []
167 dealer_hand << c2
168 dealer_hand << c4
169
170 ps = hand_score(player_hand)
171 ds = hand_score(dealer_hand)
172
173 if ps == 21 && ds == 21
174 phase = "gameover"
175 result = "push"
176 message = "Both Blackjack — Push!"
177 elsif ps == 21
178 phase = "gameover"
179 result = "blackjack"
180 chips = chips + bet + (bet / 2)
181 message = "Blackjack! You win 1.5x!"
182 else
183 phase = "playing"
184 message = "Hit or Stand?"
185 end
186
187 elsif cmd == "hit"
188 player_hand << deck.pop
189 ps = hand_score(player_hand)
190
191 if ps > 21
192 phase = "gameover"
193 result = "bust"
194 chips = chips - bet
195 message = "Bust! You lose."
196 elsif ps == 21
197 dealer_draw(dealer_hand, deck)
198 ds = hand_score(dealer_hand)
199 r, chips, message = resolve_winner(ps, ds, chips, bet)
200 result = r
201 phase = "gameover"
202 else
203 phase = "playing"
204 message = "Hit or Stand?"
205 end
⏺ Spinelはサポートしている機能を把握しました。Ruby → Spinel → C → Emscripten →
WASMのパイプラインで実装します。
設計方針:
- Ruby(Spinel対応)でゲームロジックを書き、stdioでコマンドを受け渡し
- 各ボタン操作ごとに新しいWASMインスタンスを作成(シンプルで確実)
まずディレクトリ構造と全ファイルを作成します。
⏺ Bash(mkdir -p /Users/machida/dev/bj/{src,web,generated} && ls
/Users/machida/dev/bj/)
⎿  generated/
src/
web/
⏺ Write(src/blackjack.rb)
⎿  Wrote 220 lines to src/blackjack.rb
2 # Protocol (stdin): command|chips|bet|seed|player_cards|dealer_ca
rds|deck
3 # Protocol (stdout): phase|result|chips|bet|message|player_cards|
4 # Cards encoded as 0-51: suit=card/13 (S/H/D/C), value=card%13 (A
/2/../K)
5
7 VAL_STRS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"J", "Q", "K"]
8
9 $rng = 1
… +216 lines (ctrl+o to expand)
⏺ Write(web/game.js)
⎿  Wrote 286 lines to web/game.js
2 /* Blackjack — WASM bridge + game UI logic */
3 const SUIT_SYMBOL = { S: '♠', H: '♥', D: '♦', C: '♣' };
4 const SUIT_COLOR = { S: 'black', H: 'red', D: 'red', C: 'black'
};
5 const VAL_LABELS = ['A','2','3','4','5','6','7','8','9','10','J
6 ','Q','K'];
7 // Pre-compiled WebAssembly.Module for fast re-instantiation
8 let precompiledWasm = null;
9
10 async function initWasm() {
… +296 lines (ctrl+o to expand)
⏺ Write(web/index.html)
⎿  Wrote 416 lines to web/index.html
2 <html lang="ja">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=
1">
6 <title>Blackjack — Spinel + WASM</title>
7 <style>
8 /* ── Reset & base ── */
9 *, *::before, *::after { box-sizing: border-box; margin: 0; paddi
ng: 0; }
… +420 lines (ctrl+o to expand)
⏺ 次にMakefileとビルドスクリプトを書きます。
⏺ Write(Makefile)
⎿  Wrote 56 lines to Makefile
1 SPINEL_DIR = spinel
2 SPINEL = $(SPINEL_DIR)/spinel
3 SRC_RB = src/blackjack.rb
4 GEN_C = generated/blackjack.c
5 WEB_DIR = web
6 WEB_JS = $(WEB_DIR)/blackjack.js
7 WEB_WASM = $(WEB_DIR)/blackjack.wasm
8
9 EMCC_FLAGS = -O2 \
10 -I$(SPINEL_DIR)/lib \
… +47 lines (ctrl+o to expand)
⏺ では実際にビルドを実行します。まずSpinelをクローンしてビルド、次にEmscriptenを
インストールします。
⏺ Bash(git clone https://github.com/matz/spinel spinel 2>&1 | tail -5)
⎿  Cloning into 'spinel'...
Listed 1 directory (ctrl+o to expand)
⏺ Bash(make -C spinel deps 2>&1 | tail -10 && echo "deps done")
⎿  Fetching prism v1.9.0 from rubygems.org...
curl -sL -o /tmp/prism-1.9.0.gem https://rubygems.org/gems/prism-1.9.0
.gem
… +2 lines (ctrl+o to expand)
⏺ Bash(make -C spinel 2>&1 | tail -20)
⎿  Running in the background (↓ to manage)
⎿  (timeout 2m)
⏺ Spinelのビルド中に、Emscriptenのインストールも並行で確認します。
⏺ Bash(brew install emscripten 2>&1 | tail -20)
⎿  Running in the background (↓ to manage)
⎿  (timeout 5m)
⏺ Monitor(Spinel build and Emscripten install completion)
⎿  Monitor started · task b0lzhyike · timeout 300s
Read 1 file, listed 2 directories (ctrl+o to expand)
⏺ Bash(ls spinel/spinel_codegen 2>/dev/null && echo "DONE" || echo "in
progress..."
sleep 10…)
⎿  in progress...
still building...
⎿  (timeout 1m)
⏺ Bash(# Wait for spinel_codegen binary to appear (Spinel build completing)
for i in $(seq 1 30); do…)
⎿  Waiting... (1)
Spinel built!
⎿  (timeout 3m)
⏺ Background command "make -C spinel 2&gt;&amp;1 | tail -20" completed (exit
code 0)
⏺ Spinelのビルド完了。次にRubyコードをCに変換します。
⏺ Bash(./spinel/spinel src/blackjack.rb -S > generated/blackjack.c 2>&1 && echo
"Generated C: $(wc -l < generated/blackjack.c) lines" || echo "FAILED")
⎿  Generated C: 400 lines
Read 1 file (ctrl+o to expand)
⏺ Spinelが正しくCコードを生成しました。次にEmscriptenのインストール状況を確認し
ます。
Read 1 file (ctrl+o to expand)
⏺ Bash(which emcc 2>/dev/null || (brew list emscripten 2>/dev/null | head -2))
⎿  emcc not found
⏺ Bash(brew install emscripten 2>&1 | tail -15)
⎿  ✔︎ Bottle Manifest libunistring (1.4.2)
✔︎ Bottle libunistring (1.4.2)
✔︎ Bottle Manifest glib (2.88.0)
… +13 lines (ctrl+o to expand)
⎿  (timeout 5m)
⏺ Bash(until which emcc >/dev/null 2>&1; do sleep 5; echo "waiting for emcc...";
done; emcc --version | head -1)
⎿  waiting for emcc...
waiting for emcc...
waiting for emcc...
… +22 lines (ctrl+o to expand)
⎿  (timeout 5m)
⏺ Bash(python3 --version; which python3; brew list python@3.11 2>/dev/null |
head -1; ls /opt/homebrew/bin/python3* 2>/dev/null)
⎿  Running in the background (↓ to manage)
⏺ Background command "brew install emscripten 2&gt;&amp;1 | tail -20" completed
(exit code 0)
⏺ Monitor event: "Spinel build and Emscripten install completion"
Listed 1 directory (ctrl+o to expand)
⏺ Bash(# Fix emcc to use Python 3.11 instead of system 3.9
EMCC_PATH=$(which emcc)…)
⎿  #!/bin/bash
Read 1 file, listed 1 directory (ctrl+o to expand)
⏺ Bash(/opt/homebrew/opt/python@3.14/bin/python3.14 --version 2>&1)
⎿  Python 3.14.4
⏺ Bash(emcc --version 2>&1 | head -3)
⎿  Traceback (most recent call last):
File "/opt/homebrew/Cellar/emscripten/5.0.7/libexec/emcc.py", line 3
6, in <module>
… +2 lines (ctrl+o to expand)
Read 2 files (ctrl+o to expand)
⏺ Bash(sed -n '20,35p' /opt/homebrew/Cellar/emscripten/5.0.7/libexec/emcc)
⎿  if [ -z "$_EM_PY" ]; then
_EM_PY=$(command -v python3 2> /dev/null)
fi
… +13 lines (ctrl+o to expand)
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 emcc --version
2>&1 | head -3)
⎿  emcc: warning: config file not found: /opt/homebrew/Cellar/emscripten/
5.0.7/libexec/.emscripten. You can create one by hand or run `emcc --
generate-config`
… +2 lines (ctrl+o to expand)
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 emcc
--generate-config 2>&1
cat ~/.emscripten 2>/dev/null | head -20)
⎿  An Emscripten settings file has been generated at:
/opt/homebrew/Cellar/emscripten/5.0.7/libexec/.emscripten
… +8 lines (ctrl+o to expand)
Read 1 file, listed 1 directory (ctrl+o to expand)
⏺ Bash(# Emscripten needs wasm-ld from llvm. Install it.
brew install llvm 2>&1 | tail -5 &…)
⎿  (No output)
⏺ Bash(brew install llvm binaryen 2>&1 | tail -10)
⎿  ==> Fetching downloads for: llvm and binaryen
✔︎ Bottle Manifest llvm (22.1.4)
✔︎ Bottle Manifest llvm (22.1.4)
… +10 lines (ctrl+o to expand)
⎿  (timeout 5m)
⏺ Bash(until ls /opt/homebrew/opt/llvm/bin/wasm-ld 2>/dev/null; do sleep 10;
echo "waiting for llvm..."; done; echo "llvm ready")
⎿  Running in the background (↓ to manage)
⎿  (timeout 10m)
⏺ Bash(brew install llvm binaryen 2>&1 | grep -E
"✔︎|Error|Installed|Pouring|already|done" | tail -20)
⎿  ✔︎ JSON API formula.jws.json
✔︎ JSON API cask.jws.json
Warning: llvm 22.1.4 is already installed and up-to-date.
… +3 lines (ctrl+o to expand)
⎿  (timeout 10m)
Listed 1 directory (ctrl+o to expand)
⏺ Bash(ls /opt/homebrew/opt/llvm/bin/wasm-ld 2>/dev/null || echo "wasm-ld not
found - installing"
brew install llvm 2>&1 | tail -5)
⎿  wasm-ld not found - installing
Warning: llvm 22.1.4 is already installed and up-to-date.
To reinstall 22.1.4, run:
brew reinstall llvm
⎿  (timeout 5m)
Searched for 1 pattern (ctrl+o to expand)
⏺ EmscriptenのビルトインLLVMを使うように設定します。
Read 1 file, listed 1 directory (ctrl+o to expand)
⏺ Update(/opt/homebrew/Cellar/emscripten/5.0.7/libexec/.emscripten)
⎿  Added…2(lines,sremoved.2klinesns)
9 # is not valid, but LLVM='c:\\llvm\\' and LLVM='c:/llvm/'up context
10 # are.
───── 11 ──────────────────────────────────────────────────────────────────────
12 -LLVM_ROOT = '/usr/bin' # directory
── 13 -BINARYEN_ROOT = '/usr/local' # directory ───────
12 +LLVM_ROOT = '/opt/homebrew/Cellar/emscripten/5.0.7/libexec/llvm
+/bin' # directory ffort
13 +BINARYEN_ROOT = '/opt/homebrew' # directory
14
15 # Location of the node binary to use for running the JS parts o
f the compiler.
16 # This engine must exist, or nothing can be compiled.
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 emcc --version
2>&1 | head -3)
⎿  shared:INFO: (Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld)
5.0.7-git
Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
⏺ Emscriptenが動作しました。WASMをコンパイルします。
⏺ Bash(\
EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
\
EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \
-I spinel/lib \
-O2 \
-sMODULARIZE=1 \
-sEXPORT_NAME=createBlackjack \
-sEXIT_RUNTIME=1 \
-sFORCE_FILESYSTEM=1 \
-sINVOKE_RUN=0 \
f the compiler.
16 # This engine must exist, or nothing can be compiled.
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 emcc --version
2>&1 | head -3)
⎿  shared:INFO: (Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld)
5.0.7-git
Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
⏺ Emscriptenが動作しました。WASMをコンパイルします。
⏺ Bash(\
EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \…)
⎿  Error: Exit code 1
cache:INFO: generating system headers: sysroot_install.stamp... (this will
be cached in
"/opt/homebrew/Cellar/emscripten/5.0.7/libexec/cache/sysroot_install.stamp"
for subsequent builds)
cache:INFO: - ok
In file included from generated/blackjack.c:2:
spinel/lib/sp_runtime.h:195:778: error: call to undeclared function
'malloc_trim'; ISO C99 and later do not support implicit function
declarations [-Wimplicit-function-declaration]
195 | static void sp_gc_collect(void){int full=(sp_gc_cycle%SP_GC_FULL_IN
TERVAL==0);sp_gc_cycle++;sp_gc_hdr*hh=sp_gc_old_heap;while(hh){hh->marked=0
;hh=hh->next;}sp_gc_mark_all();if(full){sp_gc_hdr**pp=&sp_gc_old_heap;sp_gc
_old_bytes=0;while(*pp){sp_gc_hdr*h=*pp;if(!h->marked){*pp=h->next;if(h->fi
nalize)h->finalize((char*)h+sizeof(sp_gc_hdr));free(h);}else{h->marked=1;sp
_gc_old_bytes+=h->size;pp=&h->next;}}}else{hh=sp_gc_old_heap;while(hh){hh->
marked=1;hh=hh->next;}}sp_gc_hdr**pp=&sp_gc_heap;sp_gc_bytes=sp_gc_old_byte
s;while(*pp){sp_gc_hdr*h=*pp;if(!h->marked){*pp=h->next;if(h->finalize)h->f
inalize((char*)h+sizeof(sp_gc_hdr));free(h);}else{h->marked=1;*pp=h->next;h
->next=sp_gc_old_heap;sp_gc_old_heap=h;sp_gc_old_bytes+=h->size;sp_gc_bytes
+=h->size;}}sp_str_sweep();if(full)malloc_trim(0);}
|
^
generated/blackjack.c:37:15: warning: equality comparison with extraneous
parentheses [-Wparentheses-equality]
37 | if ((lv_v == 0)) {
| ~~~~~^~~~
… +73 lines (ctrl+o to expand)
⏺ 2つのエラーを修正します:resolve_winnerの多値返却のタイプ推論問題とmalloc_trim
の問題。
⏺ Write(src/blackjack.rb)
⎿  Added 28 lines, removed 18 lines
81 i = n - 1
82 while i > 0
83 j = rng_next(i + 1)
84 - tmp = deck[i]
84 + tmp = deck[i]
85 deck[i] = deck[j]
86 deck[j] = tmp
87 i -= 1
...
104 end
105 end
106
107 -def resolve_winner(ps, ds, chips, bet)
108 - if ds > 21 || ps > ds
109 - ["win", chips + bet, "You win!"]
110 - elsif ps == ds
111 - ["push", chips, "Push — bet returned."]
112 - else
113 - ["lose", chips - bet, "Dealer wins."]
114 - end
115 -end
116 -
107 # --- read input ---
108 line = gets.chomp
109 parts = line.split("|")
...
163 if ps == 21 && ds == 21
164 phase = "gameover"
165 result = "push"
176 - message = "Both Blackjack — Push!"
166 + message = "Both Blackjack - Push!"
167 elsif ps == 21
168 phase = "gameover"
169 result = "blackjack"
...
186 elsif ps == 21
187 dealer_draw(dealer_hand, deck)
188 ds = hand_score(dealer_hand)
199 - r, chips, message = resolve_winner(ps, ds, chips, bet)
200 - result = r
201 - phase = "gameover"
189 + if ds > 21 || ps > ds
190 + chips = chips + bet
191 + result = "win"
192 + message = "You win with 21!"
193 + elsif ps == ds
194 + result = "push"
195 + message = "Push!"
196 + else
197 + chips = chips - bet
198 + result = "lose"
199 + message = "Dealer wins."
200 + end
201 + phase = "gameover"
202 else
203 phase = "playing"
204 message = "Hit or Stand?"
...
208 ps = hand_score(player_hand)
209 dealer_draw(dealer_hand, deck)
210 ds = hand_score(dealer_hand)
211 - r, chips, message = resolve_winner(ps, ds, chips, bet)
212 - result = r
213 - phase = "gameover"
211 + if ds > 21 || ps > ds
212 + chips = chips + bet
213 + result = "win"
214 + message = "You win!"
215 + elsif ps == ds
216 + result = "push"
217 + message = "Push!"
218 + else
219 + chips = chips - bet
220 + result = "lose"
221 + message = "Dealer wins."
222 + end
223 + phase = "gameover"
224
225 else
226 phase = "error"
⏺ Bash(./spinel/spinel src/blackjack.rb -S > generated/blackjack.c 2>&1 && echo
"Generated $(wc -l < generated/blackjack.c) lines")
⎿  Generated 387 lines
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
220 + result = "lose"
221 + message = "Dealer wins."
222 + end
223 + phase = "gameover"
224
225 else
226 phase = "error"
⏺ Bash(./spinel/spinel src/blackjack.rb -S > generated/blackjack.c 2>&1 && echo
"Generated $(wc -l < generated/blackjack.c) lines")
⎿  Generated 387 lines
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \…)
⎿  Running in the background (↓ to manage)
⎿  (timeout 2m)
⏺ Bash(cat /private/tmp/claude-501/-Users-machida-dev-bj/78b72e53-90b1-43ee-ac69
-b2a1a80b82c4/tasks/blzr7k4ud.output 2>/dev/null | tail -5; sleep 20; cat
/private/tmp/…)
⎿  generated/blackjack.c:319:20: note: use '=' to turn this equality comp
arison into an assignment
generated/blackjack.c:342:18: warning: equality comparison with extran
… +6 lines (ctrl+o to expand)
⎿  (timeout 1m)
⏺ Background command "EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13
\
emcc generated/blackjack.c \
-I spinel/lib \
-O2 \
-Dmalloc_trim\(x\)=\(\(void\)\(x\)\) \
-sMODULARIZE=1 \
-sEXPORT_NAME=createBlackjack \
-sEXIT_RUNTIME=1 \
-sFORCE_FILESYSTEM=1 \
-sINVOKE_RUN=0 \
-lm \
-o web/blackjack.js 2&gt;&amp;1 | grep -E "error:|warning:|generated" | head
-30" completed (exit code 0)
Listed 1 directory (ctrl+o to expand)
⏺ WASMの生成成功。まずRubyレベルでロジックをテストします。
⏺ Bash(echo "new|1000|100|42|||" | ruby src/blackjack.rb)
⎿  playing||1000|100|Hit or Stand?|9,45|32,35|38,40,12,47,31,50,6,15,25,1
1,28,0,48,33,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,2
2,13,34,20,14,24,4,1,43,17,23,49,3,30
⏺ Bash(echo "hit|1000|100|42|9,45|32,35|38,40,12,47,31,50,6,15,25,11,28,0,48,33,
44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,22,13,34,20,14,24,
4,1,43,17,23,…)
⎿  gameover|bust|900|100|Bust! You lose.|9,45,30|32,35|38,40,12,47,31,50,
6,15,25,11,28,0,48,33,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,3
7,18,39,22,13,34,20,14,24,4,1,43,17,23,49,3
⏺ Bash(echo "stand|1000|100|42|9,45|32,35|38,40,12,47,31,50,6,15,25,11,28,0,48,3
3,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,22,13,34,20,14,2
4,4,1,43,17,2…)
⎿  gameover|push|1000|100|Push!|9,45|32,35|38,40,12,47,31,50,6,15,25,11,2
8,0,48,33,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,22,1
3,34,20,14,24,4,1,43,17,23,49,3,30
⏺ WASMをNode.jsでテストします。
⏺ Bash(node --version 2>/dev/null)
⎿  v22.19.0
⏺ Bash(cat > /tmp/test_wasm.mjs << 'EOF'
import { readFileSync } from 'fs';…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
cat > /tmp/test_wasm.mjs << 'EOF'
import { readFileSync } from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// Load the emscripten module
const { default: createBlackjack } = await
import('/Users/machida/dev/bj/web/blackjack.js');
async function runGame(inputStr) {
const enc = new TextEncoder();
const bytes = enc.encode(inputStr + '\n');
let pos = 0;
let output = '';
const M = await createBlackjack({
noInitialRun: true,
stdin: () => pos < bytes.length ? bytes[pos++] : null,
print: (line) => { output = line; },
printErr: () => {}
});
try { M.callMain([]); } catch(e) {
if (!e || e.name !== 'ExitStatus') console.error(e);
}
return output;
}
// Test 1: new game
⏺ Bash(echo "stand|1000|100|42|9,45|32,35|38,40,12,47,31,50,6,15,25,11,28,0,48,3
3,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,22,13,34,20,14,2
4,4,1,43,17,2…)
⎿  gameover|push|1000|100|Push!|9,45|32,35|38,40,12,47,31,50,6,15,25,11,2
8,0,48,33,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,37,18,39,22,1
3,34,20,14,24,4,1,43,17,23,49,3,30
⏺ WASMをNode.jsでテストします。
⏺ Bash(node --version 2>/dev/null)
⎿  v22.19.0
⏺ Bash(cat > /tmp/test_wasm.mjs << 'EOF'
import { readFileSync } from 'fs';…)
⎿  TypeError: M.callMain is not a function
at runGame (file:///private/tmp/test_wasm.mjs:21:11)
at async file:///private/tmp/test_wasm.mjs:28:12
… +9 lines (ctrl+o to expand)
⏺ Bash(cat > /tmp/inspect_wasm.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⏺ Bash(node --version 2>/dev/null)
⎿  v22.19.0
⏺ Bash(cat > /tmp/test_wasm.mjs << 'EOF'
import { readFileSync } from 'fs';…)
⎿  TypeError: M.callMain is not a function
at runGame (file:///private/tmp/test_wasm.mjs:21:11)
at async file:///private/tmp/test_wasm.mjs:28:12
… +9 lines (ctrl+o to expand)
⏺ Bash(cat > /tmp/inspect_wasm.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Functions: [
'FS_createDataFile',
'FS_createDevice',
… +10 lines (ctrl+o to expand)
⏺ callMainをエクスポートするよう再コンパイルします。
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \
-I spinel/lib \
… +9 lines (ctrl+o to expand)
⏺ Bash(cat > /tmp/inspect_wasm.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Functions: [
'FS_createDataFile',
'FS_createDevice',
… +10 lines (ctrl+o to expand)
⏺ callMainをエクスポートするよう再コンパイルします。
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \…)
⎿  generated/blackjack.c:36:15: warning: equality comparison with extrane
ous parentheses [-Wparentheses-equality]
generated/blackjack.c:60:17: warning: equality comparison with extrane
… +9 lines (ctrl+o to expand)
⎿  (timeout 2m)
⏺ Listing 1 directory… (ctrl+o to expand)
⎿ $ ls -lh web/blackjack.js web/blackjack.wasm 2>/dev/null
⏺ Bash(cat > /tmp/test_wasm2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
cat > /tmp/test_wasm2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';
async function runGame(inputStr) {
const enc = new TextEncoder();
const bytes = enc.encode(inputStr + '\n');
let pos = 0;
let output = '';
const M = await createBlackjack({
noInitialRun: true,
stdin: () => pos < bytes.length ? bytes[pos++] : null,
print: (line) => { output = line; },
printErr: () => {}
});
try {
M.callMain([]);
} catch(e) {
if (!e || e.name !== 'ExitStatus') console.error(e);
}
return output;
}
const r1 = await runGame('new|1000|100|42|||');
⏺ Bash(EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 \
emcc generated/blackjack.c \…)
⎿  generated/blackjack.c:36:15: warning: equality comparison with extrane
ous parentheses [-Wparentheses-equality]
generated/blackjack.c:60:17: warning: equality comparison with extrane
… +9 lines (ctrl+o to expand)
⎿  (timeout 2m)
Listed 1 directory (ctrl+o to expand)
⏺ Bash(cat > /tmp/test_wasm2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  New game: playing||1000|100|Hit or Stand?|9,45|32,35|38,40,12,47,31,50
,6,15,25,11,28,0,48,33,44,10,41,21,5,29,19,7,27,51,2,36,16,26,42,8,46,
37,18,39,22,13,34,20,14,24,4,1,43,17,23,49,3,30
… +4 lines (ctrl+o to expand)
⏺ WASMが正常動作しています。game.jsのstdinハンドリングを修正します。
⏺ Write(web/game.js)
⎿  Added 144 lines, removed 186 lines
1 -/* Blackjack — WASM bridge + game UI logic */
1 +/* Blackjack — WASM bridge + game UI logic
2 + *
3 + * Pipeline: Ruby (blackjack.rb) → Spinel AOT → C → Emscripten
+ → WASM
4 + * Each game action creates a fresh WASM instance (fast becaus
+e the WASM
5 + * binary is pre-fetched and cached as an ArrayBuffer).
6 + */
7
8 const SUIT_SYMBOL = { S: '♠', H: '♥', D: '♦', C: '♣' };
4 -const SUIT_COLOR = { S: 'black', H: 'red', D: 'red', C: 'blac
-k' };
9 +const SUIT_CHARS = ['S', 'H', 'D', 'C'];
10 const VAL_LABELS = ['A','2','3','4','5','6','7','8','9','10',
'J','Q','K'];
11
7 -// Pre-compiled WebAssembly.Module for fast re-instantiation
8 -let precompiledWasm = null;
12 +// Pre-fetched WASM binary (ArrayBuffer) — avoids re-downloadi
+ng per action
13 +let wasmBinaryCache = null;
14
15 async function initWasm() {
11 - const resp = await fetch('blackjack.wasm');
12 - const buf = await resp.arrayBuffer();
13 - precompiledWasm = await WebAssembly.compile(buf);
16 + const resp = await fetch('blackjack.wasm');
17 + wasmBinaryCache = await resp.arrayBuffer();
18 }
19
20 // Run one blackjack command through a fresh WASM instance.
17 -// Returns the raw output line from stdout.
21 +// stdin = inputStr + newline
22 +// stdout = captured into `output`
23 async function wasmRun(inputStr) {
19 - const enc = new TextEncoder();
20 - const bytes = enc.encode(inputStr + '\n');
21 - let pos = 0;
22 - let output = '';
24 + const bytes = new TextEncoder().encode(inputStr + '\n');
25 + let pos = 0;
26 + let output = '';
27
24 - await new Promise((resolve) => {
25 - const opts = {
26 - noInitialRun: true,
27 - print: (line) => { output = line; },
28 - printErr: () => {},
29 - };
30 - if (precompiledWasm) opts.wasmModule = precompiledWasm;
28 + const opts = {
29 + noInitialRun: true,
30 + // stdin is read character-by-character; return null for E
+OF
31 + stdin: () => pos < bytes.length ? bytes[pos++] : null,
32 + print: (line) => { output = line; },
33 + printErr: () => {},
34 + };
35 + if (wasmBinaryCache) opts.wasmBinary = wasmBinaryCache;
36
32 - createBlackjack(opts).then((M) => {
33 - // Override stdin: provide the command bytes then EOF
34 - M.stdin = () => (pos < bytes.length ? bytes[pos++] : nul
-l);
37 + const M = await createBlackjack(opts);
38
36 - // Re-wire print in case module was already created
37 - M.print = (line) => { output = line; };
39 + try {
40 + M.callMain([]);
41 + } catch (e) {
42 + // Emscripten throws ExitStatus when main() calls exit()
43 + if (!e || e.name !== 'ExitStatus') console.error('[wasm]',
+ e);
44 + }
45
39 - try {
40 - M.callMain([]);
41 - } catch (e) {
42 - if (!e || e.name !== 'ExitStatus') console.error(e);
43 - }
44 - resolve();
45 - });
46 - });
47 -
46 return output;
47 }
48
51 -// Parse the pipe-separated output line into a state object
52 -function parseState(line, prev) {
53 - if (!line) return prev;
49 +// ── Card helpers ───────────────────────────────────────────
+───────────────────
50 +function cardSuitChar(c) { return SUIT_CHARS[Math.floor(c / 1
+3)]; }
51 +function cardValueLabel(c) { return VAL_LABELS[c % 13]; }
52 +function isRedCard(c) { const s = Math.floor(c / 13); retu
+rn s === 1 || s === 2; }
53 +
54 +function cardPoints(c) {
55 + const v = c % 13;
56 + if (v === 0) return 11;
57 + if (v >= 10) return 10;
58 + return v + 1;
59 +}
60 +
61 +function handScore(cards) {
62 + let total = 0, aces = 0;
63 + for (const c of cards) {
64 + const p = cardPoints(c);
65 + if (p === 11) aces++;
66 + total += p;
67 + }
68 + while (total > 21 && aces > 0) { total -= 10; aces--; }
69 + return total;
70 +}
71 +
72 +// ── State serialization ────────────────────────────────────
+───────────────────
73 +function encodeCards(arr) { return arr.join(','); }
74 +function decodeCards(s) { return s ? s.split(',').filter(Bo
+olean).map(Number) : []; }
75 +
76 +function buildInput(cmd, state) {
77 + const seed = Math.floor(Math.random() * 2_000_000_000) + 1;
78 + return [
79 + cmd,
80 + state.chips,
81 + state.bet,
82 + seed,
83 + encodeCards(state.playerCards),
84 + encodeCards(state.dealerCards),
85 + encodeCards(state.deck),
86 + ].join('|');
87 +}
88 +
89 +function parseOutput(line, fallback) {
90 + if (!line) return fallback;
91 const p = line.split('|');
92 return {
93 phase: p[0] || 'error',
94 result: p[1] || '',
58 - chips: parseInt(p[2]) || prev.chips,
59 - bet: parseInt(p[3]) || prev.bet,
95 + chips: parseInt(p[2]) || fallback.chips,
96 + bet: parseInt(p[3]) || fallback.bet,
97 message: p[4] || '',
61 - playerCards: decodeCards(p[5] || ''),
62 - dealerCards: decodeCards(p[6] || ''),
63 - deck: decodeCards(p[7] || ''),
98 + playerCards: decodeCards(p[5]),
99 + dealerCards: decodeCards(p[6]),
100 + deck: decodeCards(p[7]),
101 };
102 }
103
67 -function encodeCards(arr) { return arr.join(','); }
104 +// ── Game state ─────────────────────────────────────────────
+───────────────────
105 +let state = {
106 + phase: 'idle', result: '', chips: 1000, bet: 100,
107 + message: '', playerCards: [], dealerCards: [], deck: [],
108 +};
109
69 -function decodeCards(s) {
70 - if (!s) return [];
71 - return s.split(',').filter(Boolean).map(Number);
110 +async function sendCommand(cmd) {
111 + setUI(false);
112 + try {
113 + const out = await wasmRun(buildInput(cmd, state));
114 + state = parseOutput(out, state);
115 + } catch (e) {
116 + console.error(e);
117 + state.message = 'Error: ' + e.message;
118 + }
119 + render();
120 }
121
74 -function cardSuit(c) { return SUIT_CHARS[Math.floor(c / 13)];
- }
75 -function cardValue(c) { return VAL_LABELS[c % 13]; }
76 -
77 -const SUIT_CHARS = ['S','H','D','C'];
78 -
79 -function isRed(c) {
80 - const s = Math.floor(c / 13);
81 - return s === 1 || s === 2; // Hearts or Diamonds
82 -}
83 -
84 -// Build a card DOM element
122 +// ── DOM helpers ────────────────────────────────────────────
+───────────────────
123 function makeCardEl(cardIndex, hidden) {
124 const el = document.createElement('div');
125 el.className = 'card' + (hidden ? ' card-hidden' : '');
...
127 el.innerHTML = '<div class="card-back"></div>';
128 return el;
129 }
92 - const v = cardValue(cardIndex);
93 - const s = cardSuit(cardIndex);
94 - const sym = SUIT_SYMBOL[s];
95 - const color = isRed(cardIndex) ? 'red' : '';
96 - el.classList.add(color ? 'card-red' : 'card-black');
130 + const v = cardValueLabel(cardIndex);
131 + const sym = SUIT_SYMBOL[cardSuitChar(cardIndex)];
132 + el.classList.add(isRedCard(cardIndex) ? 'card-red' : 'card-b
+lack');
133 el.innerHTML =
134 `<div class="card-corner top"><span class="cv">${v}</span>
<span class="cs">${sym}</span></div>` +
135 `<div class="card-center">${sym}</div>` +
...
137 return el;
138 }
139
104 -// Render a hand into a container element
105 -function renderHand(containerId, cards, hideSecond) {
106 - const el = document.getElementById(containerId);
140 +function renderHand(id, cards, hideSecond) {
141 + const el = document.getElementById(id);
142 el.innerHTML = '';
143 cards.forEach((c, i) => {
109 - const hide = hideSecond && i === 1;
110 - const card = makeCardEl(c, hide);
111 - card.style.animationDelay = (i * 0.1) + 's';
144 + const card = makeCardEl(c, hideSecond && i === 1);
145 + card.style.animationDelay = (i * 0.12) + 's';
146 el.appendChild(card);
147 });
148 }
149
116 -// Update score badge
117 -function renderScore(elId, cards, hideSecond) {
118 - const el = document.getElementById(elId);
150 +function renderScore(id, cards, hideSecond) {
151 + const el = document.getElementById(id);
152 if (!cards.length) { el.textContent = ''; return; }
120 - if (hideSecond && cards.length >= 2) {
121 - // Only show score for visible card
122 - const visible = cards.filter((_, i) => i !== 1);
123 - el.textContent = handScore(visible);
124 - } else {
125 - el.textContent = handScore(cards);
126 - }
153 + const visible = hideSecond && cards.length >= 2 ? [cards[0]]
+ : cards;
154 + el.textContent = handScore(visible);
155 }
156
129 -function handScore(cards) {
130 - let total = 0, aces = 0;
131 - cards.forEach(c => {
132 - const v = c % 13;
133 - const pts = v === 0 ? 11 : v >= 10 ? 10 : v + 1;
134 - if (pts === 11) aces++;
135 - total += pts;
136 - });
137 - while (total > 21 && aces > 0) { total -= 10; aces--; }
138 - return total;
157 +function resultLabel(r) {
158 + const map = { win: 'You Win!', blackjack: 'Blackjack!', push
+: 'Push', bust: 'Bust', lose: 'Dealer Wins' };
159 + return map[r] || '';
160 }
161
141 -// ── Game state ──
142 -let state = {
143 - phase: 'idle',
144 - result: '',
145 - chips: 1000,
146 - bet: 100,
147 - message: '',
148 - playerCards: [],
149 - dealerCards: [],
150 - deck: [],
151 -};
152 -
153 -function buildInput(cmd) {
154 - const seed = Math.floor(Math.random() * 2000000000) + 1;
155 - return [
156 - cmd,
157 - state.chips,
158 - state.bet,
159 - seed,
160 - encodeCards(state.playerCards),
161 - encodeCards(state.dealerCards),
162 - encodeCards(state.deck),
163 - ].join('|');
164 -}
165 -
166 -async function sendCommand(cmd) {
167 - setButtonsEnabled(false);
168 - showLoading(true);
169 - try {
170 - const input = buildInput(cmd);
171 - const output = await wasmRun(input);
172 - state = parseState(output, state);
173 - render();
174 - } finally {
175 - showLoading(false);
176 - }
177 -}
178 -
162 +// ── Render ─────────────────────────────────────────────────
+───────────────────
163 function render() {
180 - const playing = state.phase === 'playing';
181 - const gameover = state.phase === 'gameover';
182 - const idle = state.phase === 'idle';
183 - const hideDealer = playing;
164 + const playing = state.phase === 'playing';
165 + const gameover = state.phase === 'gameover';
166 + const idle = state.phase === 'idle';
167
185 - renderHand('dealer-cards', state.dealerCards, hideDealer);
168 + renderHand('dealer-cards', state.dealerCards, playing);
169 renderHand('player-cards', state.playerCards, false);
187 - renderScore('dealer-score', state.dealerCards, hideDealer);
170 + renderScore('dealer-score', state.dealerCards, playing);
171 renderScore('player-score', state.playerCards, false);
172
173 document.getElementById('chips-display').textContent = state
.chips;
174 document.getElementById('bet-display').textContent = state
.bet;
175
193 - const msg = document.getElementById('message');
176 + const msg = document.getElementById('message');
177 msg.textContent = state.message || '';
195 - msg.className = 'message ' + (state.result || '');
178 + msg.className = 'message ' + (state.result || '');
179
180 // Result overlay
181 const overlay = document.getElementById('result-overlay');
...
187 overlay.className = 'result-overlay';
188 }
189
207 - // Button visibility
208 - document.getElementById('btn-deal').style.display = (idle |
-| gameover) ? '' : 'none';
209 - document.getElementById('btn-hit').style.display = playing
- ? '' : 'none';
210 - document.getElementById('btn-stand').style.display = playing
- ? '' : 'none';
211 - document.getElementById('btn-bet-up').disabled = !idle &
-& !gameover;
212 - document.getElementById('btn-bet-dn').disabled = !idle &
-& !gameover;
190 + // Buttons
191 + $('btn-deal').style.display = (idle || gameover) ? '' : 'no
+ne';
192 + $('btn-hit').style.display = playing ? '' : 'none';
193 + $('btn-stand').style.display = playing ? '' : 'none';
194 + $('btn-bet-up').disabled = playing;
195 + $('btn-bet-dn').disabled = playing;
196 + $('btn-reset').style.display = (gameover && state.chips <= 0
+) ? '' : 'none';
197
214 - // Reset chips
215 - if (state.chips <= 0 && gameover) {
216 - document.getElementById('btn-reset').style.display = '';
217 - } else {
218 - document.getElementById('btn-reset').style.display = 'none
-';
219 - }
220 -
221 - setButtonsEnabled(true);
198 + setUI(true);
199 }
200
224 -function resultLabel(r) {
225 - switch (r) {
226 - case 'win': return 'You Win!';
227 - case 'blackjack': return 'Blackjack!';
228 - case 'push': return 'Push';
229 - case 'bust': return 'Bust';
230 - case 'lose': return 'Dealer Wins';
231 - default: return '';
232 - }
233 -}
234 -
235 -function setButtonsEnabled(on) {
201 +function setUI(enabled) {
202 ['btn-deal','btn-hit','btn-stand','btn-bet-up','btn-bet-dn']
.forEach(id => {
237 - const el = document.getElementById(id);
238 - if (el) el.disabled = !on;
203 + const el = $(id);
204 + if (el) el.disabled = !enabled;
205 });
206 }
207
242 -function showLoading(on) {
243 - document.getElementById('loading').style.display = on ? '' :
- 'none';
244 -}
245 -
208 function changeBet(delta) {
209 + if (state.phase === 'playing') return;
210 const next = state.bet + delta;
248 - if (next < 10) return;
249 - if (next > state.chips) return;
211 + if (next < 10 || next > state.chips) return;
212 state.bet = next;
251 - document.getElementById('bet-display').textContent = state.b
-et;
213 + $('bet-display').textContent = state.bet;
214 }
215
216 function resetGame() {
255 - state.chips = 1000;
256 - state.bet = 100;
257 - state.phase = 'idle';
258 - state.result = '';
259 - state.message = '';
260 - state.playerCards = [];
261 - state.dealerCards = [];
262 - state.deck = [];
217 + state = { phase:'idle', result:'', chips:1000, bet:100,
218 + message:'', playerCards:[], dealerCards:[], deck:[
+] };
219 render();
220 }
221
266 -// ── Init ──
222 +function $(id) { return document.getElementById(id); }
223 +
224 +// ── Init ───────────────────────────────────────────────────
+───────────────────
225 window.addEventListener('DOMContentLoaded', async () => {
268 - showLoading(true);
226 + $('loading').style.display = '';
227
270 - document.getElementById('btn-deal') .addEventListener('click
-', () => sendCommand('new'));
271 - document.getElementById('btn-hit') .addEventListener('click
-', () => sendCommand('hit'));
272 - document.getElementById('btn-stand').addEventListener('click
-', () => sendCommand('stand'));
273 - document.getElementById('btn-bet-up').addEventListener('clic
-k', () => changeBet(10));
274 - document.getElementById('btn-bet-dn').addEventListener('clic
-k', () => changeBet(-10));
275 - document.getElementById('btn-reset') .addEventListener('clic
-k', resetGame);
228 + $('btn-deal') .addEventListener('click', () => sendCommand('
+new'));
229 + $('btn-hit') .addEventListener('click', () => sendCommand('
+hit'));
230 + $('btn-stand').addEventListener('click', () => sendCommand('
+stand'));
231 + $('btn-bet-up').addEventListener('click', () => changeBet( 1
+0));
232 + $('btn-bet-dn').addEventListener('click', () => changeBet(-1
+0));
233 + $('btn-reset') .addEventListener('click', resetGame);
234
235 try {
236 await initWasm();
237 } catch (e) {
238 console.error('WASM load failed:', e);
281 - document.getElementById('message').textContent = 'Failed t
-o load game. See console.';
239 + $('message').textContent = 'WASM load failed — see console
+.';
240 }
241
284 - showLoading(false);
242 + $('loading').style.display = 'none';
243 render();
244 });
⏺ ブラウザでテストするためにローカルサーバーを起動します。
⏺ Bash(cd web && python3 -m http.server 8080 &
sleep 1…)
⎿  blackjack.js
blackjack.wasm*
game.js
… +20 lines (ctrl+o to expand)
⏺ Bash(lsof -ti:8080 | xargs kill -9 2>/dev/null; sleep 1
cd web && python3 -m http.server 8080 &…)
⎿  (eval):cd:2: no such file or directory: web
Server running
⏺ Bash(python3 -m http.server 8080 --directory /Users/machida/dev/bj/web &
sleep 2…)
⎿  ::1 - - [30/Apr/2026 20:57:13] "GET /index.html HTTP/1.1" 200 -
<!DOCTYPE html>
<html lang="ja">
… +3 lines (ctrl+o to expand)
⏺ Bash(open http://localhost:8080/)
⎿  (No output)
⏺ ブラウザを開きました。Node.jsで包括的なゲームフローをテストします。
⏺ Bash(cat > /tmp/test_full.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
cat > /tmp/test_full.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';
import { readFileSync } from 'fs';
const wasmBinary =
readFileSync('/Users/machida/dev/bj/web/blackjack.wasm');
async function run(inputStr) {
const bytes = new TextEncoder().encode(inputStr + '\n');
let pos = 0, output = '';
const M = await createBlackjack({
noInitialRun: true,
wasmBinary: wasmBinary.buffer,
stdin: () => pos < bytes.length ? bytes[pos++] : null,
print: (line) => { output = line; },
printErr: () => {},
});
try { M.callMain([]); } catch(e) {
if (!e || e.name !== 'ExitStatus') console.error(e);
}
return output;
}
function parse(line) {
const p = line.split('|');
return { phase: p[0], result: p[1], chips: +p[2], bet: +p[3],
msg: p[4], player: p[5], dealer: p[6], deck: p[7] };
}
function handScore(cards) {
const VAL_LABELS =
['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
let total=0, aces=0;
for (const c of cards.split(',').map(Number)) {
const v=c%13, p=v===0?11:v>=10?10:v+1;
if(p===11) aces++;
total+=p;
}
while(total>21&&aces>0){total-=10;aces--;}
return total;
}
console.log('=== Blackjack WASM Tests ===\n');
// Test 1: New game
const g1 = parse(await run('new|1000|100|42|||'));
console.log(`[1] New game: phase=${g1.phase} chips=${g1.chips}
bet=${g1.bet}`);
console.log(` Player ${g1.player} (${handScore(g1.player)}) | Dealer
${g1.dealer} (hidden)`);
console.assert(g1.phase === 'playing', 'Should be playing');
console.assert(g1.chips === 1000, 'Chips should be 1000');
// Test 2: Stand (gameover)
const g2 = parse(await run(`stand|${g1.chips}|${g1.bet}|99|${g1.player}|${
g1.dealer}|${g1.deck}`));
console.log(`\n[2] Stand: phase=${g2.phase} result=${g2.result}
chips=${g2.chips}`);
console.log(` Player ${handScore(g1.player)} vs Dealer
${handScore(g2.dealer.split(',').map(Number))}`);
console.log(` Message: ${g2.msg}`);
console.assert(g2.phase === 'gameover', 'Should be gameover');
// Test 3: New game → Hit until bust or win
let state = parse(await run('new|1000|100|12345|||'));
console.log(`\n[3] New game: player=${state.player}
(${handScore(state.player)}), dealer=${state.dealer}`);
let hitCount = 0;
while (state.phase === 'playing' && hitCount < 10) {
state = parse(await run(`hit|${state.chips}|${state.bet}|${hitCount+1}|$
{state.player}|${state.dealer}|${state.deck}`));
console.log(` Hit: player=${state.player}
score=${handScore(state.player)} phase=${state.phase}${state.result?'
result='+state.result:''}`);
hitCount++;
}
// Test 4: Multiple new games with different seeds
console.log('\n[4] 5 new games:');
for (let seed=1; seed<=5; seed++) {
const g = parse(await run(`new|1000|100|${seed*1000}|||`));
const ps = handScore(g.player);
console.log(` seed=${seed*1000} player=${g.player} (${ps})
phase=${g.phase}${g.result?' result='+g.result:''}`);
}
// Test 5: Blackjack detection (1.5x payout)
console.log('\n[5] Chips after blackjack payout: expect > 1000');
let found = false;
for (let seed=1; seed<=200; seed++) {
⏺ Bash(python3 -m http.server 8080 --directory /Users/machida/dev/bj/web &
sleep 2…)
⎿  ::1 - - [30/Apr/2026 20:57:13] "GET /index.html HTTP/1.1" 200 -
<!DOCTYPE html>
<html lang="ja">
… +3 lines (ctrl+o to expand)
⏺ Bash(open http://localhost:8080/)
⎿  (No output)
⏺ ブラウザを開きました。Node.jsで包括的なゲームフローをテストします。
⏺ Bash(cat > /tmp/test_full.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Error: Exit code 1
=== Blackjack WASM Tests ===
[1] New game: phase=playing chips=1000 bet=100
Player 9,45 (17) | Dealer 32,35 (hidden)
[2] Stand: phase=gameover result=push chips=1000
file:///private/tmp/test_full.mjs:31
for (const c of cards.split(',').map(Number)) {
^
… +6 lines (ctrl+o to expand)
⏺ Bash(cat > /tmp/test_full2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  Running…
────────────────────────────────────────────────────────────────────────────────
Bash command
cat > /tmp/test_full2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';
import { readFileSync } from 'fs';
const wasmBinary =
readFileSync('/Users/machida/dev/bj/web/blackjack.wasm');
async function run(inputStr) {
const bytes = new TextEncoder().encode(inputStr + '\n');
let pos = 0, output = '';
const M = await createBlackjack({
noInitialRun: true, wasmBinary: wasmBinary.buffer,
stdin: () => pos < bytes.length ? bytes[pos++] : null,
print: (l) => { output = l; }, printErr: () => {},
});
try { M.callMain([]); } catch(e) {
if (!e || e.name !== 'ExitStatus') console.error(e);
}
return output;
}
function parse(line) {
const p = line.split('|');
return { phase:p[0], result:p[1], chips:+p[2], bet:+p[3],
msg:p[4], player:p[5]||'', dealer:p[6]||'', deck:p[7]||'' };
}
function score(cardStr) {
if (!cardStr) return 0;
const cards = cardStr.split(',').filter(Boolean).map(Number);
let total=0, aces=0;
for (const c of cards) {
const v=c%13, p=v===0?11:v>=10?10:v+1;
if(p===11) aces++; total+=p;
}
while(total>21&&aces>0){total-=10;aces--;} return total;
}
// Test suite
let g = parse(await run('new|1000|100|42|||'));
console.log(`New game: player=${g.player}(${score(g.player)})
dealer=${g.dealer}(?) phase=${g.phase}`);
console.assert(g.phase==='playing');
g = parse(await
run(`stand|${g.chips}|${g.bet}|1|${g.player}|${g.dealer}|${g.deck}`));
console.log(`Stand: result=${g.result} chips=${g.chips} msg=${g.msg}`);
console.assert(g.phase==='gameover');
// Hit sequence
let s = parse(await run('new|1000|100|12345|||'));
console.log(`\nHit sequence: player=${s.player}(${score(s.player)})`);
for(let i=0; i<5 && s.phase==='playing'; i++){
s = parse(await
run(`hit|${s.chips}|${s.bet}|${i}|${s.player}|${s.dealer}|${s.deck}`));
console.log(` Hit ${i+1}: ${s.player}(${score(s.player)})
phase=${s.phase} ${s.result||''}`);
}
// Find blackjack
let found=false;
for(let seed=1; seed<=200; seed++){
const g2 = parse(await run(`new|1000|100|${seed}|||`));
if(g2.result==='blackjack'){
console.log(`\nBlackjack at seed=${seed}: chips=${g2.chips} (1.5x
payout, expect 1150)`);
console.assert(g2.chips===1150);
found=true; break;
}
}
if(!found) console.log('No blackjack in 200 seeds');
// Chip loss test
let s2 = parse(await run('new|1000|100|99|||'));
if(s2.phase==='playing'){
s2 = parse(await
run(`hit|${s2.chips}|${s2.bet}|1|${s2.player}|${s2.dealer}|${s2.deck}`));
if(s2.result==='bust'){
console.log(`\nBust: chips went from 1000 to ${s2.chips} (expect
⎿  Error: Exit code 1
=== Blackjack WASM Tests ===
[1] New game: phase=playing chips=1000 bet=100
Player 9,45 (17) | Dealer 32,35 (hidden)
[2] Stand: phase=gameover result=push chips=1000
file:///private/tmp/test_full.mjs:31
for (const c of cards.split(',').map(Number)) {
^
… +6 lines (ctrl+o to expand)
⏺ Bash(cat > /tmp/test_full2.mjs << 'EOF'
import { default as createBlackjack } from
'/Users/machida/dev/bj/web/blackjack.js';…)
⎿  New game: player=9,45(17) dealer=32,35(?) phase=playing
Stand: result=push chips=1000 msg=Push!
… +8 lines (ctrl+o to expand)
⏺ 全テスト通過。MakefileをWASMビルドフラグで更新します。
⏺ Write(~/dev/bj/Makefile)
⎿  Added 26 lines, removed 18 lines
2 SPINEL = $(SPINEL_DIR)/spinel
3 SRC_RB = src/blackjack.rb
4 GEN_C = generated/blackjack.c
5 -WEB_DIR = web
6 -WEB_JS = $(WEB_DIR)/blackjack.js
7 -WEB_WASM = $(WEB_DIR)/blackjack.wasm
5 +WEB_JS = web/blackjack.js
6 +WEB_WASM = web/blackjack.wasm
7
8 +# Python 3.10+ required by emscripten
9 +EMSDK_PYTHON ?= /opt/homebrew/opt/python@3.13/bin/python3.13
10 +EMCC = EMSDK_PYTHON=$(EMSDK_PYTHON) emcc
11 +
12 EMCC_FLAGS = -O2 \
13 -I$(SPINEL_DIR)/lib \
14 + -Dmalloc_trim\(x\)=\(\(void\)\(x\)\) \
15 -sMODULARIZE=1 \
16 -sEXPORT_NAME=createBlackjack \
17 -sEXIT_RUNTIME=1 \
18 -sFORCE_FILESYSTEM=1 \
19 -sINVOKE_RUN=0 \
20 + "-sEXPORTED_RUNTIME_METHODS=['callMain','FS']" \
21 -lm
22
18 -.PHONY: all clean serve spinel-check wasm-check
23 +.PHONY: all clean serve check
24
25 all: $(WEB_JS)
26
22 -# ── 1. Clone & build Spinel ──
27 +# ── 1. Clone & build Spinel ──────────────────────────────────
+────────────────
28 $(SPINEL_DIR)/Makefile:
29 git clone https://github.com/matz/spinel $(SPINEL_DIR)
30
31 $(SPINEL): $(SPINEL_DIR)/Makefile
32 cd $(SPINEL_DIR) && make deps && make
33
29 -# ── 2. Compile Ruby → C via Spinel ──
34 +# ── 2. Ruby → C (Spinel AOT) ─────────────────────────────────
+───────────────
35 $(GEN_C): $(SRC_RB) $(SPINEL)
36 mkdir -p generated
37 + @echo "→ Compiling Ruby to C via Spinel..."
38 $(SPINEL) $(SRC_RB) -S > $(GEN_C)
39 + @echo " Generated $(GEN_C) ($$(wc -l < $(GEN_C)) lines)"
40
34 -# ── 3. Compile C → WASM via Emscripten ──
41 +# ── 3. C → WASM (Emscripten) ─────────────────────────────────
+───────────────
42 $(WEB_JS): $(GEN_C)
36 - emcc $(GEN_C) $(EMCC_FLAGS) -o $(WEB_JS)
37 -
38 -# ── Helpers ──
39 -spinel-check:
40 - @which ruby >/dev/null 2>&1 || (echo "ruby not found"; exit 1
-)
41 - @echo "Ruby: $$(ruby --version)"
42 -
43 -wasm-check:
43 @which emcc >/dev/null 2>&1 || (echo "emcc not found. Run: br
ew install emscripten"; exit 1)
45 - @echo "emcc: $$(emcc --version | head -1)"
44 + @echo "→ Compiling C to WASM via Emscripten..."
45 + $(EMCC) $(GEN_C) $(EMCC_FLAGS) -o $(WEB_JS)
46 + @echo " Generated $(WEB_JS) and $(WEB_WASM)"
47
47 -# Run a local dev server (requires Python 3)
48 +# ── Dev server ───────────────────────────────────────────────
+────────────────
49 serve:
50 @echo "Open http://localhost:8080"
50 - cd $(WEB_DIR) && python3 -m http.server 8080
51 + python3 -m http.server 8080 --directory web
52
53 +# ── Sanity check ─────────────────────────────────────────────
+────────────────
54 +check:
55 + @which ruby >/dev/null 2>&1 && echo "✓ ruby $$(ruby --versi
+on)" || echo "✗ ruby not found"
56 + @which emcc >/dev/null 2>&1 && (EMSDK_PYTHON=$(EMSDK_PYTHON)
+ emcc --version 2>&1 | head -1 | sed 's/^/✓ /') || echo "✗ emcc
+ not found (brew install emscripten)"
57 + @which node >/dev/null 2>&1 && echo "✓ node $$(node --versi
+on)" || echo "✗ node not found"
58 + @test -f $(WEB_WASM) && echo "✓ WASM $(WEB_WASM)" || echo "✗
+ WASM not built (run: make)"
59 +
60 clean:
61 rm -rf generated $(WEB_JS) $(WEB_WASM)
62
⏺ ブラウザでUIが正常に動作するか確認します。
⏺ Bash(# Check if server is running
curl -s http://localhost:8080/ | grep -c "createBlackjack\|Blackjack" &&
echo "Server OK")
⎿  2
Server OK
⏺ Bash(curl -s http://localhost:8080/blackjack.wasm | head -c 8 | xxd | head -2)
⎿  00000000: 0061 736d 0100 0000 .asm....
❯ このアプリについて技術的な解説して
Searched for 2 patterns, listed 1 directory (ctrl+o to expand)
⏺ まず最終確認を行います。
Listed 1 directory (ctrl+o to expand)
⏺ Bash(curl -s http://localhost:8080/ > /dev/null && echo "HTTP server OK at
http://localhost:8080")
⎿  HTTP server OK at http://localhost:8080
⏺ ゲームが完成しました。http://localhost:8080 で遊べます。
---
技術解説
コンパイルパイプライン
blackjack.rb ──[Spinel AOT]──▶ blackjack.c ──[Emscripten]──▶
blackjack.wasm
(Ruby) (C, ~400行)
(WebAssembly, 36KB)
4段階の変換:
1. Ruby (src/blackjack.rb) — ゲームロジックをRubyで記述。標準的なRubyに見える
が、Spinelの型推論が効く範囲の構文に限定(Fiberなし、メタプログラミングなし)
2. Spinel AOT Compiler (matz/spinel) —
Ruby全体プログラム型推論を行い、C言語に変換。例えば hand_score(hand) →
sp_hand_score(sp_IntArray* lv_hand)
に変換。型安全なCコードを生成し、ネイティブバイナリより10〜80倍高速に動作する
3. Emscripten — 生成されたCコードを wasm32-unknown-emscripten
ターゲットでコンパイル。-sMODULARIZE=1 でJSファクトリ関数としてラップ
4. Browser (JS) — ボタンクリックごとに createBlackjack()
で新しいWASMインスタンスを生成。stdインターフェース経由でコマンドを渡し、標準
出力からゲーム状態を読む
通信プロトコル(stdin/stdout)
JS → WASM: "hit|1000|100|48291|9,45|32,35|38,40,12,..."
└──┘ └──┘ └──┘ └───┘ └──┘ └───┘ └─────┘
cmd chips bet seed player dealer 残デッキ
WASM → JS: "gameover|bust|900|100|Bust!|9,45,30|32,35|..."
カードは0〜51の整数で符号化(suit=card÷13, value=card%13)。Spinel生成CはRuby
のガベージコレクタ付きランタイムをそのままWASMに載せている。
Spinelとの互換性対応
- malloc_trim() がEmscriptenにないため -Dmalloc_trim(x)=((void)(x)) でno-op化
- 乱数は rand() の代わりに独自LCG($rng * 1664525 + 1013904223 mod
2147483647)をRuby側で実装し、JSからシードを渡す
- 多値返却(r, chips, msg = func())はSpinelの型推論でPolyArrayになりCの型エラ
ーが出るため、インライン展開して回避
✻ Crunched for 51m 14s · 2 shells still running
❯ pinelと は使ってる?
⏺ はい、実際に使っています。証拠を確認します。
⏺ Searching for 1 pattern, reading 1 file, listing 1 directory… (ctrl+o to
expand)
Searched for 2 patterns, read 1 file, listed 1 directory (ctrl+o to expand)
"Spinelバイナリ:" && file /Users/machida/dev/bj/spinel/spinel_codegen
⏺ はい、ちゃんと使っています。
流れを整理すると:
generated/blackjack.c | awk '{print $6,$7,$8,$9}'
1. src/blackjack.rb — ゲームロジックをRubyで書く
2. ./spinel/spinel blackjack.rb -S —
Spinelがそれを読んで全体プログラム型推論し、Cコードを生成する
3. emcc generated/blackjack.c — EmscriptenがそのCをWASMにコンパイル
4. ブラウザ — WASMとして実行
たとえば hand_score(hand) というRubyの関数が、Spinelによって
sp_hand_score(sp_IntArray* lv_hand)
という型付きCに変換されています。Spinelが「このhandはInteger配列だ」と推論した
結果が型として現れています(sp_IntArray*)。
直接JavaScriptで書いたわけではなく、RubyコードをSpinelでCにAOTコンパイルし、そ
のCをWASMに載せています。
✻ Cooked for 25s · 2 shells still running
※ recap: Blackjack game running at http://localhost:8080 — Ruby logic compiled
by Spinel to C, then by Emscripten to WASM. Next: open the browser and play a
round to verify the UI works end-to-end. (disable recaps in /config)
❯ READMEを充実させて
Searched for 1 pattern, read 1 file, listed 1 directory (ctrl+o to expand)
⎿  You're out of extra usage · resets 12:40am (Asia/Tokyo)
✻ Churned for 15s · 2 shells still running
❯ READMEを充実させて
Read 1 file, listed 1 directory (ctrl+o to expand)
⏺ Write(~/dev/bj/README.md)
⎿  Added 263 lines, removed 1 line
1 -# bj
1 +# Blackjack — Spinel AOT + WebAssembly
2 +
3 +ブラウザで遊べるブラックジャック。ゲームロジックを **Ruby** で
+書き、
4 +[Spinel](https://github.com/matz/spinel) AOT コンパイラで **C*
+* に変換し、
5 +[Emscripten](https://emscripten.org/) で **WebAssembly** にコ
+ンパイルする。
6 +
7 +```
8 +blackjack.rb ──[Spinel]──▶ blackjack.c ──[Emscripten]──▶ b
+lackjack.wasm
9 + Ruby AOT C WASM
+ ブラウザ実行
10 + (230行) コンパイル (387行) (36 KB)
11 +```
12 +
13 +---
14 +
15 +## 動かし方
16 +
17 +### 必要なもの
18 +
19 +| ツール | 用途 | インストール |
20 +|--------|------|-------------|
21 +| Ruby 3.x | Spinel のビルドに必要 | `brew install ruby` |
22 +| Emscripten | C → WASM コンパイル | `brew install emscripten`
+ |
23 +| Python 3.10+ | Emscripten の依存 | `brew install python@3.13
+` |
24 +| GNU make | ビルド自動化 | macOS 標準搭載 |
25 +
26 +### ビルド
27 +
28 +```sh
29 +# Spinel のクローン&ビルド → Ruby → C → WASM をすべて一発で
30 +make
31 +
32 +# 動作確認
33 +make check
34 +
35 +# 開発サーバー起動
36 +make serve
37 +# → http://localhost:8080 をブラウザで開く
38 +```
39 +
40 +初回は Spinel のビルド(数分)と Emscripten のキャッシュ生成が
+走る。
41 +2回目以降は差分だけ再コンパイルされる。
42 +
43 +### macOS で emcc が Python エラーになる場合
44 +
45 +Homebrew の emcc は Python 3.10+ を要求するが、macOS 標準の py
+thon3 は
46 +3.9 系のことがある。`EMSDK_PYTHON` を指定して回避する:
47 +
48 +```sh
49 +EMSDK_PYTHON=/opt/homebrew/opt/python@3.13/bin/python3.13 make
50 +```
51 +
52 +`Makefile` 冒頭の `EMSDK_PYTHON` 変数を編集しても良い。
53 +
54 +---
55 +
56 +## ファイル構成
57 +
58 +```
59 +.
60 +├── src/
61 +│ └── blackjack.rb # ゲームロジック(Ruby・Spinel 方
+言)
62 +├── generated/
63 +│ └── blackjack.c # Spinel が生成した C コード ※編集
+不要
64 +├── web/
65 +│ ├── index.html # UI(HTML + CSS インライン)
66 +│ ├── game.js # JS ブリッジ(WASM ↔ DOM)
67 +│ ├── blackjack.js # Emscripten 生成グルーコード ※編
+集不要
68 +│ └── blackjack.wasm # WebAssembly バイナリ ※編集不要
69 +├── spinel/ # matz/spinel クローン(make 時に
+自動取得)
70 +└── Makefile
71 +```
72 +
73 +---
74 +
75 +## Spinel とは
76 +
77 +[Spinel](https://github.com/matz/spinel) は **Ruby の AOT(Ahe
+ad-of-Time)コンパイラ**。
78 +Ruby ソースに全体プログラム型推論をかけて最適化された C コード
+を生成し、
79 +ネイティブバイナリにコンパイルする。作者は Ruby の作者・まつも
+とゆきひろ(matz)。
80 +
81 +- CRuby 比で **幾何平均 11.6 倍高速**(計算集約的な処理では最
+大 86 倍)
82 +- コンパイラ自身も Ruby で書かれており、**セルフホスト**を達成
+している
83 +- 生成物はスタンドアロンバイナリ(ランタイム不要)
84 +
85 +このプロジェクトでは Spinel の **`-S` フラグ**(C コードを std
+out に出力)を使い、
86 +生成された C を Emscripten に渡すことで WASM として動作させて
+いる。
87 +
88 +---
89 +
90 +## コンパイルパイプラインの詳細
91 +
92 +### ステップ 1: Ruby → C(Spinel)
93 +
94 +```sh
95 +./spinel/spinel src/blackjack.rb -S > generated/blackjack.c
96 +```
97 +
98 +Spinel は Ruby を解析して**全体プログラム型推論**を行い、型付
+きの C コードを生成する。
99 +たとえば `hand_score(hand)` という Ruby の関数は:
100 +
101 +```ruby
102 +# Ruby (src/blackjack.rb)
103 +def hand_score(hand)
104 + total = 0
105 + aces = 0
106 + i = 0
107 + while i < hand.length
108 + p = card_points(hand[i])
109 + if p == 11
110 + aces += 1
111 + end
112 + total += p
113 + i += 1
114 + end
115 + while total > 21 && aces > 0
116 + total -= 10
117 + aces -= 1
118 + end
119 + total
120 +end
121 +```
122 +
123 +`hand` が Integer の配列だと推論され、以下の C に変換される:
124 +
125 +```c
126 +// C (generated/blackjack.c) — Spinel が自動生成
127 +static mrb_int sp_hand_score(sp_IntArray * lv_hand) {
128 + mrb_int lv_total = 0;
129 + mrb_int lv_aces = 0;
130 + mrb_int lv_i = 0;
131 + mrb_int lv_p = 0;
132 + SP_GC_SAVE();
133 + SP_GC_ROOT(lv_hand);
134 + lv_total = 0; lv_aces = 0; lv_i = 0;
135 + mrb_int _t1 = sp_IntArray_length(lv_hand);
136 + while ((lv_i < _t1)) {
137 + lv_p = sp_card_points(sp_IntArray_get(lv_hand, lv_i));
138 + if ((lv_p == 11)) { lv_aces += 1; }
139 + lv_total += lv_p;
140 + lv_i += 1;
141 + }
142 + while (((lv_total > 21) && (lv_aces > 0))) {
143 + lv_total -= 10;
144 + lv_aces -= 1;
145 + }
146 + return lv_total;
147 +}
148 +```
149 +
150 +引数型(`sp_IntArray*`)、変数型(`mrb_int`)、GC ルートの登録
+(`SP_GC_ROOT`)まで
151 +すべて Spinel が推論・生成する。
152 +
153 +### ステップ 2: C → WASM(Emscripten)
154 +
155 +```sh
156 +emcc generated/blackjack.c \
157 + -I spinel/lib \
158 + -O2 \
159 + -Dmalloc_trim(x)=((void)(x)) \ # Emscripten にない関数を n
+o-op 化
160 + -sMODULARIZE=1 \ # JS ファクトリ関数としてラ
+ップ
161 + -sEXPORT_NAME=createBlackjack \
162 + -sEXIT_RUNTIME=1 \
163 + -sFORCE_FILESYSTEM=1 \
164 + -sINVOKE_RUN=0 \ # main() を自動実行しない
165 + "-sEXPORTED_RUNTIME_METHODS=['callMain','FS']" \
166 + -lm \
167 + -o web/blackjack.js
168 +```
169 +
170 +Spinel のランタイム(`sp_runtime.h`)には GC、文字列管理、配列
+実装が含まれており、
171 +これをまるごと WASM に載せている。
172 +
173 +### ステップ 3: WASM ↔ JavaScript(stdin/stdout ブリッジ)
174 +
175 +ゲームロジック(Spinel 生成 WASM)と UI(JavaScript)は **stdi
+n/stdout** で通信する。
176 +ボタンを押すたびに新しい WASM インスタンスを生成し(プリコンパ
+イル済みバイナリを再利用)、
177 +コマンドを stdin として渡し、出力を stdout から受け取る。
178 +
179 +```
180 +JS WASM (Spinel 生成)
181 +─────────────────────────────────────────────────────
182 +stdin ──▶ "hit|1000|100|48291|9,45|32,35|38,40,…"
183 + └──┘ └──┘ └──┘ └───┘ └──┘ └───┘ └──┘
184 + cmd chips bet seed player dealer 残デッキ
185 +
186 +stdout ◀── "gameover|bust|900|100|Bust! You lose.|9,45,30|32
+,35|…"
187 + └──────┘ └──┘ └──┘ └──┘ └───────────┘
188 + phase result chips bet message
189 +```
190 +
191 +カードは 0〜51 の整数で符号化(`suit = card / 13`、`value = ca
+rd % 13`)。
192 +
193 +```js
194 +// game.js の核心部分
195 +async function wasmRun(inputStr) {
196 + const bytes = new TextEncoder().encode(inputStr + '\n');
197 + let pos = 0, output = '';
198 +
199 + const M = await createBlackjack({
200 + noInitialRun: true,
201 + stdin: () => pos < bytes.length ? bytes[pos++] : null,
202 + print: (line) => { output = line; },
203 + printErr: () => {},
204 + });
205 +
206 + try { M.callMain([]); }
207 + catch (e) { /* ExitStatus は正常終了 */ }
208 +
209 + return output;
210 +}
211 +```
212 +
213 +---
214 +
215 +## Spinel 方言について
216 +
217 +Spinel は Ruby の全機能をサポートしているわけではない。
218 +このプロジェクトで**避けた**Ruby の機能と、その理由:
219 +
220 +| 避けた機能 | 理由 |
221 +|-----------|------|
222 +| `Fiber` | `ucontext.h` を使うため WASM 非対応 |
223 +| `rand` | Spinel から Ruby の `rand` を呼べないため、LCG を自
+前実装 |
224 +| 多値返却 `a, b = func()` | 混合型配列(`PolyArray`)が生成さ
+れ Emscripten で型エラー |
225 +| `eval` / メタプログラミング | Spinel 非対応 |
226 +
227 +乱数は次の LCG(線形合同法)をRubyで実装し、JS から seed を渡
+している:
228 +
229 +```ruby
230 +$rng = seed
231 +def rng_next(max)
232 + $rng = ($rng * 1664525 + 1013904223) % 2147483647
233 + $rng % max
234 +end
235 +```
236 +
237 +---
238 +
239 +## ゲームルール
240 +
241 +- 初期チップ **$1,000**、最低ベット **$10**
242 +- ブラックジャック(最初の2枚で 21)は **1.5倍**払い
243 +- ディーラーは **17 以上**になるまで必ずヒット(ソフト 17 も含
+む)
244 +- ベット変更は各ゲームの開始前のみ可能
245 +
246 +---
247 +
248 +## 開発メモ
249 +
250 +### Emscripten で `malloc_trim` がないエラー
251 +
252 +Spinel のランタイムヘッダ(`sp_runtime.h`)は非 Apple / 非 Win
+dows 環境で
253 +`malloc_trim(0)` を呼ぶが、Emscripten の libc に宣言がない。
254 +コンパイル時に `-Dmalloc_trim(x)=((void)(x))` で no-op に置き
+換えている。
255 +
256 +### 毎回 WASM インスタンスを作り直す理由
257 +
258 +Spinel が生成する C コードの `main()` は1回限りの実行を前提と
+しており、
259 +グローバル変数の初期化が再実行に対応していない。
260 +そのため `callMain()` を複数回呼ぶと GC 状態が壊れる可能性があ
+る。
261 +対策として毎回新しいインスタンスを生成しているが、WASM バイナ
+リは
262 +`ArrayBuffer` としてキャッシュするため再取得は発生しない。
263 +カードゲーム程度のデータ量では体感できるパフォーマンス差はない
+。
Read 1 file (ctrl+o to expand)
⏺ 263行のREADMEを書きました。内容:
- 動かし方 — 必要ツール一覧、make 一発ビルド手順、Python
バージョン問題の回避策
- ファイル構成 — 各ファイルの役割
- Spinel とは — 何者か、なぜ速いか、このプロジェクトでの使い方
- コンパイルパイプライン詳細 — Ruby→C
の型推論の様子を実際のコード対比で説明、Emscripten フラグの意味、stdin/stdout
ブリッジの仕組み
- Spinel 方言について — 使えなかった Ruby
機能とその理由(Fiber・rand・多値返却)
- ゲームルール — ベット・ブラックジャック配当・ディーラールール
- 開発メモ — malloc_trim 問題とインスタンス毎回生成の理由
✻ Worked for 1m 16s · 2 shells still running
❯ AIの署名なしでコミットプッシュ
⏺ Bash(cd /Users/machida/dev/bj && git status)
⎿  generated/
Makefile
README.md
… +13 lines (ctrl+o to expand)
⏺ Bash(git diff README.md | head -20)
⎿  diff --git a/README.md b/README.md
index 1d80fb1..39491e4 100644
--- a/README.md
… +21 lines (ctrl+o to expand)
⏺ Bash(git add README.md && git commit -m "READMEを充実させる" && git push)
⎿  [main 62c6aec] READMEを充実させる
1 file changed, 263 insertions(+), 1 deletion(-)
To https://github.com/machida/bj.git
33c222d..62c6aec main -> main
⏺ プッシュしました。
✻ Cogitated for 13s · 2 shells still running
❯ これをGitHubページで公開ってできる?
⏺ Bash(git ls-files web/ && echo "---" && cat .gitignore 2>/dev/null || echo "no
.gitignore")
⎿  web/blackjack.js
web/blackjack.wasm
web/game.js
… +3 lines (ctrl+o to expand)
⏺ Bash(gh repo view machida/bj --json name,url,defaultBranchRef 2>/dev/null ||
gh repo view --json name,url,defaultBranchRef 2>/dev/null)
⎿  {
"defaultBranchRef": {
"name": "main"
… +4 lines (ctrl+o to expand)
⏺ web/ 以下のファイルはすでにコミット済みなので、GitHub Pages の「Source: main
ブランチの /docs フォルダ」か「/ (root)」を使うか、または GitHub Actions
で自動デプロイする方法があります。
一番簡単なのは web/ を docs/ にリネームしてプッシュし、Pages の Source を main
/ docs に設定する方法です。やりますか?
それとも GitHub Actions でビルドからデプロイまで自動化する形(src/blackjack.rb
を push するたびに Spinel + Emscripten でビルドして公開)にしますか?
✻ Sautéed for 14s · 2 shells still running
❯ 一番カンタンな方法でやって。READMEも更新して。必要であればテストも更新して。
できたらコミットプッシュ。
⏺ web/ → docs/ にリネームして GitHub Pages の Source を設定する方法でやります。
Searched for 1 pattern (ctrl+o to expand)
⏺ Bash(git mv web docs && git status)
⎿  On branch main
Your branch is up to date with 'origin/main'.
… +6 lines (ctrl+o to expand)
⏺ Update(Makefile)
⎿  Added 2 lines, removed 2 lines
2 SPINEL = $(SPINEL_DIR)/spinel
3 SRC_RB = src/blackjack.rb
4 GEN_C = generated/blackjack.c
5 -WEB_JS = web/blackjack.js
6 -WEB_WASM = web/blackjack.wasm
5 +WEB_JS = docs/blackjack.js
6 +WEB_WASM = docs/blackjack.wasm
7
8 # Python 3.10+ required by emscripten
9 EMSDK_PYTHON ?= /opt/homebrew/opt/python@3.13/bin/python3.13
⏺ Update(Makefile)
⎿  Added 1 line, removed 1 line
48 # ── Dev server ───────────────────────────────────────────────
────────────────
49 serve:
50 @echo "Open http://localhost:8080"
51 - python3 -m http.server 8080 --directory web
51 + python3 -m http.server 8080 --directory docs
52
53 # ── Sanity check ─────────────────────────────────────────────
────────────────
54 check:
⏺ Update(Makefile)
⎿  Added 1 line, removed 1 line
55 @which ruby >/dev/null 2>&1 && echo "✓ ruby $$(ruby --versi
on)" || echo "✗ ruby not found"
56 @which emcc >/dev/null 2>&1 && (EMSDK_PYTHON=$(EMSDK_PYTHON)
emcc --version 2>&1 | head -1 | sed 's/^/✓ /') || echo "✗ emcc
not found (brew install emscripten)"
57 @which node >/dev/null 2>&1 && echo "✓ node $$(node --versi
on)" || echo "✗ node not found"
58 - @test -f $(WEB_WASM) && echo "✓ WASM $(WEB_WASM)" || echo "✗
- WASM not built (run: make)"
58 + @test -f $(WEB_WASM) && echo "✓ WASM $(WEB_WASM)" || echo "✗
+ WASM not built yet — run: make"
59
60 clean:
61 rm -rf generated $(WEB_JS) $(WEB_WASM)
⏺ 次にREADMEを更新します。GitHub PagesのURLとdocs/への変更を反映します。
Read 1 file (ctrl+o to expand)
⏺ Update(README.md)
⎿  Added 2 lines
1 # Blackjack — Spinel AOT + WebAssembly
2
3 +**▶ https://machida.github.io/bj/**
4 +
5 ブラウザで遊べるブラックジャック。ゲームロジックを **Ruby** で書
き、
6 [Spinel](https://github.com/matz/spinel) AOT コンパイラで **C**
に変換し、
7 [Emscripten](https://emscripten.org/) で **WebAssembly** にコン
パイルする。
⏺ Update(README.md)
⎿  Added 2 lines, removed 2 lines
63 │ └── blackjack.rb # ゲームロジック(Ruby・Spinel 方言
64 ├── generated/
65 │ └── blackjack.c # Spinel が生成した C コード ※編集
不要
66 -├── web/
66 +├── docs/
67 │ ├── index.html # UI(HTML + CSS インライン)
68 │ ├── game.js # JS ブリッジ(WASM ↔ DOM)
...
166 -sINVOKE_RUN=0 \ # main() を自動実行しない
167 "-sEXPORTED_RUNTIME_METHODS=['callMain','FS']" \
168 -lm \
169 - -o web/blackjack.js
169 + -o docs/blackjack.js
170 ```
171
172 Spinel のランタイム(`sp_runtime.h`)には GC、文字列管理、配列
実装が含まれており、
⏺ コミット・プッシュして、GitHub Pages を有効化します。
⏺ Bash(git add Makefile README.md && git status)
⎿  On branch main
Your branch is up to date with 'origin/main'.
… +8 lines (ctrl+o to expand)
⏺ Bash(git commit -m "web/ を docs/ にリネームして GitHub Pages に対応" && git
push)
⎿  [main 0454317] web/ を docs/ にリネームして GitHub Pages に対応
6 files changed, 8 insertions(+), 6 deletions(-)
rename {web => docs}/blackjack.js (100%)
… +5 lines (ctrl+o to expand)
⏺ Bash(gh api repos/machida/bj/pages \
--method POST \…)
⎿  Error: Exit code 1
{"message":"Invalid request.\n\nInvalid property /source:
`\"{\\\"branch\\\":\\\"main\\\",\\\"path\\\":\\\"/docs\\\"}\"` is not of
type `object`.","documentation_url":"https://docs.github.com/rest/pages/pag
es#create-a-apiname-pages-site","status":"422"}gh: Invalid request.
Invalid property /source: `"{\"branch\":\"main\",\"path\":\"/docs\"}"` is
not of type `object`. (HTTP 422)
⏺ Bash(gh api repos/machida/bj/pages \
--method POST \…)
⎿  {
"url": "https://api.github.com/repos/machida/bj/pages",
"status": null,
… +13 lines (ctrl+o to expand)
⏺ GitHub Pages が有効になりました。数分でデプロイが完了します。
https://machida.github.io/bj/
でアクセスできるようになります(初回デプロイは1〜3分ほどかかります)。
やったこと:
1. web/ → docs/ にリネーム(GitHub Pages は main ブランチの /docs
フォルダを直接サーブできる)
2. Makefile と README.md のパスを更新
3. README 冒頭に公開 URL を追記
4. gh api で Pages を有効化(Source: main / /docs)
✻ Churned for 49s · 2 shells still running
❯ https://machida.github.io/bj/ が404です
⏺ Bash(gh api repos/machida/bj/pages 2>&1)
⎿  {
"url": "https://api.github.com/repos/machida/bj/pages",
"status": "building",
… +13 lines (ctrl+o to expand)
error: .error.message, updated_at}')
⎿  Error: Blocked: sleep 30 followed by: gh api
repos/machida/bj/pages/builds/latest --jq '{status, error: .error.message,
updated_at}'. To wait for a condition, use Monitor with an until-loop (e.g.
`until <check>; do sleep 2; done`). To wait for a command you started, use
run_in_background: true. Do not chain shorter sleeps to work around this
block.
⏺ Monitor(GitHub Pages ビルド完了待ち)
⎿  Monitor started · task bf9ygrbq7 · timeout 120s
⏺ Bash(gh api repos/machida/bj/pages/builds/latest --jq '{status, error:
.error.message}')
⎿  {
"error": null,
"status": "building"
}
⏺ ビルド中です。完了したら通知が来ます。そのまま少しお待ちください。
✻ Worked for 19s · 2 shells, 1 monitor still running
⏺ Monitor event: "GitHub Pages ビルド完了待ち"
⏺ Bash(gh api repos/machida/bj/pages/builds/latest --jq '{status, error:
.error.message, updated_at}')
⎿  {
"error": null,
"status": "building",
… +2 lines (ctrl+o to expand)
⏺ 2分以上「building」のままなのは変です。リポジトリの可視性を確認します。
⏺ Bash(gh repo view machida/bj --json isPrivate,visibility)
⎿  {
"isPrivate": false,
"visibility": "PUBLIC"
}
⏺ Bash(gh api repos/machida/bj/pages --jq '{status, build_type, source}')
⎿  {
"build_type": "legacy",
"source": {
… +5 lines (ctrl+o to expand)
⏺ legacy ビルドが止まっています。GitHub Actions 方式に切り替えます。
⏺ Bash(gh api repos/machida/bj/pages \
--method PUT \…)
⎿  (No output)
⏺ Bash(gh api repos/machida/bj/pages --jq '{status, build_type}')
⎿  {
"build_type": "workflow",
"status": "building"
}
⏺ GitHub Actions 方式になったので、デプロイワークフローを追加します。
⏺ Bash(mkdir -p /Users/machida/dev/bj/.github/workflows)
⎿  Done
⏺ Write(.github/workflows/pages.yml)
⎿  Wrote 36 lines to .github/workflows/pages.yml
1 name: Deploy to GitHub Pages
2
3 on:
4 push:
5 branches: [main]
6 workflow_dispatch:
7
8 permissions:
9 contents: read
10 pages: write
… +26 lines (ctrl+o to expand)
⏺ Bash(git add .github/workflows/pages.yml && git commit -m "GitHub Actions で
Pages デプロイを設定" && git push)
⎿  [main ee513f2] GitHub Actions で Pages デプロイを設定
1 file changed, 36 insertions(+)
create mode 100644 .github/workflows/pages.yml
… +2 lines (ctrl+o to expand)
⏺ Monitor(GitHub Actions Pages デプロイ完了待ち)
⎿  Monitor started · task bkekhrqrc · timeout 180s
⏺ プッシュしました。GitHub Actions がトリガーされて docs/
の内容をそのまま公開します。完了したらお知らせします。
✻ Sautéed for 41s · 2 shells, 1 monitor still running
⏺ Monitor event: "GitHub Actions Pages デプロイ完了待ち"
⏺ デプロイ成功しました。
https://machida.github.io/bj/ にアクセスしてみてください。
✻ Baked for 2s · 2 shells still running
⏺ Monitor "GitHub Actions Pages デプロイ完了待ち" stream ended
⏺ デプロイ完了の通知が確認できました。 https://machida.github.io/bj/
でゲームが公開されています。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment