
2026/04/12 19:01
「ドゥーム(Doome)」というゲームが、カーリング(Curling)の盤上でプレイされている様子。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本書では、標準的なコンピュータターミナルウィンドウ内においてクラシックゲーム「DOOM」をストリーミング再生する方法について説明しています。この方法により、重厚なグラフィックドライバや外部クライアントの使用を不要とします。単一の TCP 接続を通じて入力を受け取り、ビデオフレームを送信することを同時に実現することで、
cURL や bash のようなツールを用いた単純な HTTP リクエストによって双方向のゲームプレイが可能になります。この技術的画期的進歩により、コマンドラインインターフェース内だけで商業用ゲームを体験できるようになり、ハードウェア要件を大幅に低減させるとともに特定のオペレーティングシステムへの依存性を排除します。バックエンドを構築するには現代の Node.js 環境と具体的なソースファイルが必要ですが、一旦実行を開始すれば、プレイヤーはテキストコマンドを通じて W A S D の移動操作やレベル間の変換を行うことができます。cURL で管理されるセッションを終了させる際にはターミナルの状態を手動でリセットして通常の動作を復元する必要がある場合もありますが、この軽量なソリューションはリソースが限られた利用者にも複雑なゲームを追加ソフトウェアやドライバをインストールせずにプレイできる手段を提供します。
Text to translate:
Summary: The text describes a method to stream the classic game "DOOM" directly inside a standard computer terminal window, eliminating the need for heavy graphics drivers or external clients. By using a single TCP connection to simultaneously handle incoming input and outgoing video frames, this setup achieves bi-directional gameplay via simple HTTP requests with tools like
cURL and bash. This technical breakthrough allows users to experience commercial gaming entirely within a command-line interface, drastically lowering hardware requirements and removing dependencies on specific operating systems. While building the backend requires a modern Node.js environment and specific source files, once running, players can control WASD movement and warp between levels directly through text commands. Although quitting a session managed purely by cURL may require manually resetting the terminal state to restore normal behavior, this lightweight solution offers an accessible way for those with limited resources to play complex games without installing additional software or drivers.本文
cURL DOOM: HTTP プロトコルでプレイする DOOM
curl を介してブラウザやターミナルから遊べる DOOM。HTTP サーバーが DOOM のフレームを ANSI の半ブロック文字に変換し、それを cURL を使ってターミナルへストリーミング送信します。
- インストール不要
- 依存要件は
とcurl
のみbash
遊び方 2 つ
1. 親しみやすい方法:curl | bash
curl | bashcurl -sL http://localhost:3000 | bash
仕組みはどうなっているのでしょうか? GET
/ リクエストはコンテンツネゴシエーションによって処理されます。curl は取得元のホスト名を __SERVER__ 変数として置き換えた play.sh スクリプトを受け取ります。このスクリプトが、キー入力を逐次処理する /tick ループを実行し、stty の設定、代替画面モード、カーソル操作、そしてクリーンアップを処理します。同じ URL にアクセスしたブラウザは、ワンライナーを表示するだけの小さなランディングページを受け取ります。
2. クソツメな方法:純粋な curl
とシェルループなし
curlstty -echo -icanon min 1 time 0 && curl -sN -X POST -T - localhost:3000/play
- デフォルトでは小さな画面サイズです。設定方法は後述します。
プレイ開始は、何らかのキーを押して。
- 終了するには
を押してください(※ `'q' キーは無効です)。Ctrl+C - ターミナルを正常な状態に戻すには、完了後に
コマンドを実行してください。reset
デフォルトの小さな画面ではなく、列数と行数を変更したい場合は、以下のように入力します:
curl -sN -X POST -T - "localhost:3000/play?cols=200&rows=60"
仕組みはどうなっているのでしょうか? 1 つのストリーミング HTTP リクエストで、双方向通信を行います。入力キーはリクエスト本文(ボディ)を介して送信され、ANSI フレームはレスポンス本文を介して返送されます。キー入力を逐次処理するラッパーや、キー入力ごとに往復するオーバーヘッドがありません。これは単一の TCP 接続で、送信側と受信側の両方を同時に行うものです。
ただし注意点として、シェルは通常ターミナルを「熟練モード(cooked mode)」に設定しており、これにより (a)
stdin がバッファリングされ Enter キーを押すまで curl はキー入力を検知できず、(b) ターミナルに入力した文字がそのままフレームの上に重畳して表示されるという問題が発生します。そのため、まずターミナルを生モード(raw mode)に切り替える必要があり、作業終了後にも元に戻す必要があります。したがって、curl の前に stty コマンドを実行し、完了後に reset を呼び出す必要があるのです。
よりクリーンに、少し長い手順で行うこともできます:
( stty -echo -icanon min 1 time 0 < /dev/tty trap 'stty sane < /dev/tty' EXIT INT TERM curl -sN -X POST -T - localhost:3000/play < /dev/tty )
(行数と列数の設定方法については、上記をご参照ください。)
キーの押したまま操作する場合: サーバーは各キー入力を受信した後、最後のバイトを 150 ms 経過後に解放します。したがって
w キーを押したままにすると、滑らかに前方に進むことができます。終了するには Ctrl+C で切断してください。トラップ処理により、いずれの場合でもターミナルは正常な状態に戻ります。
スムーズさについて
/play エンドポイントはデフォルトで FPS 15 に設定されています。これは、-T - オプションを使用した curl が、stdin に入力がない間(read(stdin) でブロックされている間)レスポンスソケットを処理しないため、キー入力があるまでフレームがカーネルの送信バッファに溜まってしまうからです。何かを押すと一度に大量のデータ(バースト)が排出されます。15 FPS は、ターミナルが次のフレームが到着する前に各フレームをレンダリングできる程度に、そのバーストを小さく抑えるために設定されています。
上書きする方法:
curl ... "http://localhost:3000/play?cols=200&rows=60&fps=25" …
各フレームはカーソルを画面先頭に移動して元の位置で塗りつぶす方式(各フレームごとにスクリーン全体をクリアしない)なので、遅いターミナルが追いつけなくても、最悪の場合「引き裂かれた」フレーム(上部が N+1 フレーム、下部が N フレーム)しか見られず、白画面にはなりません。
もしプレイせずに見るだけ(入力に関係なくフレームがストリーミングされる)場合は、
stty の設定も不要で、-T - によるブロックも発生しないため、デフォルトの 15 FPS は十分滑らかで、より高画質にすることも可能です:
curl -sN -X POST "http://localhost:3000/play?cols=200&rows=60&fps=30"
DOOM が自分自身で動き出します。飽きたら
Ctrl+C で終了してください。
実装概要
ターミナル cURL DOOM サーバー ------------- ---------------- curl GET / ----------> play.sh <---------- (__SERVER__ を書き換えたもの) bash の pipe に接続 stty raw モード キー入力を読み取る curl POST /tick?s=&key= --------> doom セッションにキー情報を送り込み <-------- doom のフレームバッファから ANSI フレームを受信 /dev/tty に出力 ループ処理を継続
サーバーは各セッションにつき、1 つの
doomgeneric プロセスを持続します。各セッションには以下のものが割り当てられます:
- テキストコマンド(
: キー入力、K
: アドバンス TIC、T
: フレームダンプ、F
: 終了)をプッシュする標準入力パイプ、Q - fd 3 の専用フレームパイプ(stderr での printf ロギングがバイナリフレームバッファを汚染しないよう確保)、
- ヘッドレスなバックエンドが
の内で増やされる仮想的なクロック(doom の「次の TIC まで待つ」ループが即座にブロック解除されるように)。DG_SleepMs
doom から出力される各フレームは、640×400 ピクセルの BGRA 形式(1 MB)です。サーバーはこのデータをターミナルの列数×行数×2 のピクセルグリッドにダウンスAMPLE します。その際に上半分ブロックのグリフ
▀ を使用し、フォアグラウンドが上画素、バックグラウンドが下画素という方式により、無料で垂直解像度を実質 2 倍にしています。また、色が実際に変化する時だけ SGR エスケープシーケンスを送信するため、レスポンスサイズは約 5 分の 1 に圧縮されます。
アイドル中のセッションは 60 秒後に除去(リイプト)されます。Node プロセスを強制終了すると、子プロセスとして実行されているすべての doom も共に終了します。
サーバーのセットアップ
(これはゲーム自体を遊ぶためのものではありません。)
要件
- Node.js 18 以上
- C コンパイラ (
/cc
/clang
) とgccmake - doom1 シェアウェア WAD ファイル
- doomgeneric のソースコード
ビルドと実行
# 1. Node の依存ライブラリをインストール npm install # 2. ヘッドレスな doom バイナリをビルド(一度だけ実行) cd doomgeneric/doomgeneric && make -f Makefile.server && cd ../.. # 3. サーバーを起動 npm start # -> cURL DOOM は http://localhost:3000 で動作開始 # -> プレイ方法: curl -sL http://localhost:3000 | bash
コードは
doom1.wad(自由に配布されているシェアウェアエピソード)を前提としています。他の WAD を使用する場合は、それをプロジェクトのルートディレクトリに配置し、index.js 内の WAD 定数を編集してください。
コントロール
| キー | 動作 |
|---|---|
/ | 前進 |
/ | 後退 |
/ | 左に回転 |
/ | 右に回転 |
/ | 左へ横移動 / 右へ横移動 |
| 攻撃 (発砲) |
スペース / | アイテム使用 / ドア開閉 |
| オートマップ |
| メニューの確定 |
| メニュー / 戻る |
/ | メニューダイアログでの「はい」/「いいえ」 |
| 終了 |
セッションは Hurt me plenty エピソード(
-warp 1 1 -skill 3)のエピソード 1 メイン 1 (E1M1) に即座に移行するため、タイトル画面やメニュー操作をスキップできます。
カスタマイズ
| 環境変数 | デフォルト | 効果 |
|---|---|---|
| | クライアントが接続するサーバーURL |
| ターミナル幅 | 描画ビューポートの幅を強制設定 |
| ターミナル高さ - 1 | 描画ビューポートの高さを強制設定 |
| | サーバー側:リスニングポート番号 |
クライアントは
stty size < /dev/tty を使用してターミナルサイズを自動検出します(ioctl(TIOCGWINSZ) でカーネルの TTY 状態を読み取り、失敗した場合は tput と $LINES/$COLUMNS にフォールバック)。DOOM のネイティブ解像度(半ブロックグリフ下)は 320x200 ピクセル(= ターミナルセル 320列 x 100行)であり、それより大きいサイズでもクリッピングされます(アップスケーリングのみになるため)。
大きなターミナルで小さなビューポートを強制するには:
DOOM_COLS=120 DOOM_ROWS=40 ./doom.sh
リモートサーバーに接続したい場合は:
DOOM_SERVER=https://doom.example.com ./doom.sh
HTTP API
すべてのルートは
?cols=N&rows=N パラメータを受け付け、描画ビューポートを上書きすることができます。
- GET
: コンテンツネゴシエーションされたランディングページ(curl 用スクリプト、ブラウザ用 HTML)/ - POST
: セッションを作成し、初号フレームを返送する。レスポンスヘッダー/new
にセッション ID が含まれるX-Session - POST
: 1 キーの操作をプッシュし、約 5 TIC を進める。次回のフレームを返送する/tick?s=&key= - POST
: バイディレクショナルなストリーミング処理。リクエストボディにキー入力を、レスポンスボディに ANSI フレームを送信(デフォルト FPS 15、範囲は 5-35)/play?cols=&rows=&fps= - POST
: セッションを即時に破棄する(60 秒待機なし)/quit?s= - GET
:/health
のようなセッション数を返す{"sessions": N}
クレジット
- 作成者:Sawyer X.
- DOOM: id Software, 1993
- doomgeneric: ozkl 氏による、カスタムレンダリングバックエンドを切り替えられるための抽象化。
- doom1.wad: 自由に配布可能なシェアウェアエピソード
コミット履歴
コミット履歴は短く、説明不足です。これは私が恥ずかしい C のミス、ひどい JavaScript、恥ずかしいタイプミスのため、履歴を書き換えたからです。
その作業には結構時間がかかりました...
なぜか? だって DOOM があるからです。