自宅NWをKeepalivedとdnsmasqで作り直した話

背景 自宅のネットワーク構成が辛かった。 具体的には以下の問題があった。 ルータが1台構成で、死んだら自宅NW全滅 メンテナンスで定期的に落としたいとかあっても、コストが大きい DNS/DHCPがルータに同居していて、役割が混在している WireGuardで外部接続していたが、外部からの入り方がだるい。 特に「冗長化したい」という気持ちは何年も前からあったが、DNSとDHCPの同期という難問の前に何度も挫折してきた。 結論からいえば、DNS,DHCPの冗長化は諦めた。 諦めた上で出口のルータとDNS、DHCPは役割を分けた上で出口のルータとWIFIルータのみ冗長化を実施した。 設計方針 過去の失敗から学んだ一番の教訓はやはり「DNS/DHCPの冗長化は難しい」ということだ。 理論上可能ではあるが、私は万年初級自宅インフラエンジニアでいつまで経ってもできる目処が立たない。 それでいて、生成AIという過ぎた兵器を持ってしても、この問題は解決しなかったので、難しいというよりは不可能とした。 2台のサーバでDHCPを冗長化しようとすると、リースの同期問題が必ず発生する。同期ツールを使ったりしたが、どうしても同期しきれなかった。 そこで今回は思い切って諦めた上で役割を分離する方針にした。 ルータ(冗長化) → GWとTailscaleだけ担当 AP → hostapdのみ DNS/DHCP → 専用機1台に集約(冗長化しない) DNSとDHCPは単一障害点になるが、自宅ラボであれば許容範囲だと判断した。 てかどうしようもない。 構成 使用機材はすべてRaspberry Pi。封印していたタワーからも引っ張り出した。 ホスト 役割 ルータA (MASTER) Keepalived + Tailscale ルータB (BACKUP) Keepalived + Tailscale DNS/DHCP機 dnsmasq専用 AP x2 WIFI冗長化 ネットワークセグメントはひとつ。VIPをデフォルトゲートウェイ兼DNSとして各クライアントに配布する。 Keepalivedの設定 まずルータ2台にKeepalivedを入れる。 sudo apt install -y keepalived MASTER側の設定: vrrp_instance VI_1 { state MASTER interface lan-if virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass xxxxxxxx } virtual_ipaddress { 192.168.xx.1/24 } } BACKUP側は state BACKUP と priority 90 に変えるだけ。シンプルだ。 ...

April 18, 2026 · 2 min

ドメイン購入後のやり残し作業 - メールとDNS設定編

wasutech.dev を公開したはいいが、メール周りを何もしていなかった。放置するとなりすましに悪用される可能性があるので、Cloudflare Email Routing・SPF・DMARCを設定した。 Cloudflare Email Routing 自前のメールサーバーを建てるのはコストがかかる。Cloudflareには Email Routing という機能があり、@wasutech.dev 宛のメールを既存のGmailなどに転送できる。無料。 仕組みとしては、Cloudflareが自動でMXレコードを追加し、受信したメールを指定のアドレスへ転送する。 dig MX wasutech.dev # → isaac.mx.cloudflare.net 等が返ってくる MXレコードはドメインレベルの情報なので、転送先のメールアドレスは外部に公開されない。dig で見えるのはCloudflareのサーバーだけで、「どこに転送しているか」は誰にもわからない。 設定はCloudflareのダッシュボードから数クリックで完了する。 転送先のアクションは3種類から選べる。 アクション 動作 Forward to email 指定のメールアドレスへ転送 Drop 受信して即破棄。転送しない Send to Worker Cloudflare Workersで独自処理 今回はメールを受け取る必要がないため Drop に設定した。スパム対策にもなるし、転送先アドレスを管理する必要もない。Workers連携は受信メールをSlackに流したり、自動返信を実装したりする場合に使う。 SPF - 送信元を証明するレコード SPFとは SPF(Sender Policy Framework) は、「このドメインからメールを送信していいサーバーはどこか」を定義するDNSレコード。 なぜ必要か。メールのプロトコル(SMTP)は設計上、送信元アドレスを自由に詐称できる。つまり誰でも [email protected] を名乗ってメールを送れる。SPFはこれを受信側が検証できるようにする仕組み。 受信側のメールサーバーは、届いたメールの送信元IPアドレスを確認し、そのドメインのSPFレコードに記載されたIPと照合する。一致しなければ怪しいメールとして処理できる。 設定したレコード v=spf1 include:_spf.mx.cloudflare.net ~all 各要素の意味: 要素 意味 v=spf1 SPFバージョン1 include:_spf.mx.cloudflare.net CloudflareのメールサーバーからのSMTPを許可 ~all 上記以外は「疑わしい」扱い(ソフトフェイル) ~all は疑わしいメールを迷惑メール扱いにする。-all にすると完全拒否になるが、DMARCと組み合わせて制御するのが一般的。 確認: dig TXT wasutech.dev # v=spf1 include:_spf.mx.cloudflare.net ~all DMARC - SPFの結果を使って何をするか決めるレコード DMARCとは DMARC(Domain-based Message Authentication, Reporting, and Conformance) は、SPF(やDKIM)の検証結果に基づいて、受信側が「そのメールをどう扱うか」を指示するレコード。 ...

April 14, 2026 · 1 min

Cloudflare Workers でサイト監視 + Discord通知を作った

外形監視をどこに任せるか迷った。ラズパイでやるか、Cloudflareに任せるか。 外から見えるサービスの監視なら せっかくなのでCloudflare Workersにする。 自宅NWそこそこ落ちたりするので・・・。 ラズパイと比較した Cloudflare Workers ラズパイ 向いてる監視 外形監視(外からの死活確認) 内部NW監視 コスト 無料 電気代のみ メンテ ほぼゼロ たまに落ちる ローカルNW確認 ❌ ✅ 最小間隔 1分 自由 今回は wasutech.dev と blog.wasutech.dev、techblog.wasutech.dev の3つを監視したい。 Cloudflare Workers 無料枠で十分な理由 公式ドキュメント - Limits によると、無料プランは以下のとおり。 Cron Triggers: 5個まで リクエスト: 1日10万回まで CPU時間: 10ms/invocation 今回のユースケースは「HTTPリクエスト投げてステータスコード確認して Discord Webhook 叩く」だけなので、CPU時間は 2〜3ms で収まる。5分間隔で3ドメイン監視しても 1日864リクエストなので余裕。 コード全文 // src/index.ts const TARGETS = [ { name: "wasutech.dev", url: "https://wasutech.dev" }, { name: "blog.wasutech.dev", url: "https://blog.wasutech.dev" }, { name: "techblog.wasutech.dev", url: "https://techblog.wasutech.dev" }, ]; export default { async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) { const results = await Promise.allSettled( TARGETS.map((t) => check(t.name, t.url)) ); const failures = results .map((r, i) => ({ result: r, target: TARGETS[i] })) .filter(({ result }) => result.status === "rejected" || (result.status === "fulfilled" && !result.value.ok) ); if (failures.length > 0) { const lines = failures.map(({ result, target }) => { const detail = result.status === "rejected" ? (result.reason as Error).message : `HTTP ${(result.value as Response).status}`; return `🔴 ${target.name} | ${detail}`; }); await notify(env.DISCORD_WEBHOOK, lines.join("\n")); } }, }; async function check(name: string, url: string): Promise<Response> { const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(10000), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res; } async function notify(webhookUrl: string, msg: string) { await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: msg }), }); } interface Env { DISCORD_WEBHOOK: string; } # wrangler.toml name = "wasutech-monitor" main = "src/index.ts" compatibility_date = "2025-01-01" [triggers] crons = ["*/5 * * * *"] Promise.allSettled を使った理由 Promise.all だと1つが失敗した時点で残りを待たずに throw する。 Promise.allSettled なら3つ並列でチェックして、全部の結果を集めてからまとめて通知できる。 1メッセージにまとめることでDiscordがスパムにならない。 ...

April 13, 2026 · 2 min

CloudflareアラートをWorker + Gemini APIでDiscordに要約通知する

Cloudflareの通知をDiscord Webhookに流していたら、通知が大量に届くようになってしまった。重要なアラートが埋もれるので、Cloudflare Workers + Gemini APIで要約してから投稿するようにした。 方針 Cloudflareの通知設定で追加できるアラートはとりあえず全部有効にしている。各アラートが実際に役立つかは様子を見ながら判断する予定。 ただしセキュリティアラートはだいたい30分に数回届いたため、現時点では通知をスキップするようにした。それ以外のアラートは引き続き通知して様子見中。 構成 Cloudflare Alert → Worker受信 → フィルタリング(スキップ対象なら早期リターン) → Gemini API で日本語要約 → Discord Webhook に送信 前提 Cloudflare WorkersにDiscord Webhook通知のWorkerが既にある Google AI StudioのAPIキーを持っている モデル選定 Geminiのモデルは料金帯がいくつかある。 gemini-2.5-pro > gemini-2.5-flash > gemini-2.5-flash-lite Cloudflareアラートの要約程度であれば gemini-2.5-flash-lite で十分。一番安い。 最新のモデル名は公式ドキュメントで確認すること。 https://ai.google.dev/gemini-api/docs/models 実装 フィルタリングの考え方 頻度の高いアラートはWorker側でスキップできるようにしている。SKIP_ALERT_TYPES に列挙したタイプが一致した場合、Gemini APIを呼ばずに早期リターンする。不要なAPI呼び出しも減るのでコスト面でも良い。 どのアラートがどの alert_type を持つかはCloudflareの公式ドキュメントで確認できる。 https://developers.cloudflare.com/notifications/notification-available/ Worker コード // スキップしたいアラートタイプ(頻度が高くて邪魔なものを列挙) const SKIP_ALERT_TYPES = [ "security_alerts", // セキュリティアラート:30分に数回来るので無効化中 ]; async function summarizeWithGemini(apiKey, input) { const prompt = `以下のCloudflareアラートを日本語で3行以内に要約してください。重要度(🔴高/🟡中/🟢低)も判定してください。\n\n${input}`; const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite-preview-06-17:generateContent?key=${apiKey}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) } ); const data = await res.json(); console.log("Gemini response:", JSON.stringify(data)); return data.candidates?.[0]?.content?.parts?.[0]?.text || "要約失敗"; } export default { async fetch(request, env) { if (request.method !== "POST") { return new Response("ok"); } let body; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400 }); } // アラートタイプによるフィルタリング const alertType = body.text?.alert_type || body.alert_type || ""; if (SKIP_ALERT_TYPES.some(t => alertType.includes(t))) { console.log(`Skipped alert type: ${alertType}`); return new Response("skipped"); } // Geminiには常にbody全体を渡す const input = JSON.stringify(body).slice(0, 500); const summary = await summarizeWithGemini(env.GEMINI_API_KEY, input); const message = { username: "Cloudflare", embeds: [{ title: body.text?.title || body.text?.description || "Cloudflare Notification", description: summary, color: 0xF6821F, timestamp: new Date().toISOString() }] }; await fetch(env.DISCORD_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); return new Response("ok"); } } シークレット登録 wrangler secret put GEMINI_API_KEY wrangler secret put DISCORD_WEBHOOK 動作確認 curl -X POST https://<your-worker>.workers.dev \ -H "Content-Type: application/json" \ -d '{"text": {"title": "テスト通知", "description": "これはテストです"}}' スキップ確認: ...

April 12, 2026 · 2 min

Cloudflareの全通知をDiscord Webhookに飛ばす

CloudflareのNotificationsはUIから1個ずつ設定するのがとにかくだるい。 種別も多いし、手動でDiscord Webhookを登録していくのは非現実的。 Cloudflare APIとWorkersを組み合わせて全通知を一括登録する。 構成 Cloudflare Notifications ↓ Cloudflare Worker (受け口) ↓ Discord Webhook Cloudflare NotificationsはWebhook送信に対応しているので、Workerを受け口にしてDiscordに転送する。 1. Discord Webhookを作成 通知を飛ばしたいDiscordチャンネルの設定から作成する。 チャンネル設定 → Integrations → Webhooks → New Webhook → Copy Webhook URL 2. Cloudflare Workerを作成 Cloudflareダッシュボードから Workers & Pages → Create → Hello World で作成する。 名前は cf-notify-discord など適当につけてDeploy。 エディタ画面で以下のコードに置き換える。 export default { async fetch(request, env) { if (request.method !== "POST") { return new Response("ok"); } let body; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400 }); } const message = { username: "Cloudflare", embeds: [{ title: body.text?.title || "Cloudflare Notification", description: body.text?.description || JSON.stringify(body, null, 2), color: 0xF6821F, timestamp: new Date().toISOString() }] }; await fetch(env.DISCORD_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); return new Response("ok"); } } Discord WebhookのURLはWorkerのSettings → Variables and Secretsでシークレットとして登録する。 ...

April 12, 2026 · 3 min

WaylandでモニターがマイクとスピーカーとしてOSに認識される問題をWirePlumberで無効化する

問題 ふとDesktopの画面を見るとモニターがマイクとスピーカーとしてOSに認識されていた。 誤って爆音で音が再生されるリスクが気になったため無効化することにした。 ついでにマイクも有効にする意味がない環境だったので止めた。 純粋な開発PCで動画とかも見ないそこそこ特殊?な環境なので最悪読み込まないなら何でもいい状態。 環境 OS: ArchLinux サウンドサーバー: PipeWire + WirePlumber 0.5.14 GPU: AMD Ryzen(APU) 問題のデバイス: AMD/ATI Raven/Raven2/Fenghuang HDMI/DP Audio Controller 原因 HDMI/DisplayPortには映像だけでなく音声も伝送できる仕様(Audio over HDMI)がある。 LinuxはこれをALSAレベルで別サウンドカードとして認識するため、 PipeWireがそのまま拾ってオーディオデバイスとして公開してしまう。 調査 認識されているカードを確認 pactl list cards short 49 alsa_card.pci-0000_04_00.1 alsa 50 alsa_card.pci-0000_04_00.6 alsa 2枚のサウンドカードが認識されている。詳細を確認する。 pactl list cards | grep -A 30 "alsa_card.pci-0000_04_00" 結果を整理すると: PCI アドレス ベンダー 説明 用途 0000:04:00.1 AMD/ATI Raven HDMI/DP Audio Controller モニター側(不要) 0000:04:00.6 AMD + Realtek ALC269VB Ryzen HD Audio Controller 本物のオンボードサウンド 0000:04:00.1 の alsa_mixer_name が ATI R6xx HDMI であることからも、 これがHDMI経由のオーディオデバイスだと確定できる。 ...

April 11, 2026 · 1 min

AdGuard HomeをProxmox LXCに立ててTailscale経由でDNSブロックする

概要 はてな匿名ダイアリーとウーバーイーツをインフラレベルで封鎖したかった。 AdGuard HomeをProxmox LXCに立てて、Tailscale経由でDNSブロックする構成を作った。 環境 Proxmox VE Tailscale導入済み AdGuard Home v0.108.0 手順 1. AdGuard Home LXCをスクリプト一発で作成 Proxmoxのノードシェルで以下を実行する。 bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/adguard.sh)" community-scripts/ProxmoxVEが提供するスクリプト。 LXCのコンテナ作成からAdGuard Homeのインストールまで全自動でやってくれる。 デフォルト構成はDebian 13、CPU 1コア、RAM 512MB、HDD 2GB。DNS用途なら十分。 2. LXCにTUNデバイスを追加する TailscaleはWireGuardベースのVPNで、動作に/dev/net/tunが必要。 /dev/net/tunはLinuxの仮想ネットワークデバイス(TUNデバイス)。通常のネットワークデバイス(eth0等)は物理NICに紐づいているが、TUNはソフトウェアで作った仮想NIC。 Tailscaleは以下の流れで通信を処理する。 通信をTailscaleプロセスが横取り WireGuardで暗号化 暗号化したパケットを相手に送る この「横取り」の実装に/dev/net/tunを使う。TUNデバイスを通してカーネルのネットワークスタックとTailscaleプロセスがやり取りする仕組みになっている。 unprivileged LXCはセキュリティ上の理由でホストのデバイスに触れないようになっているため、明示的に/dev/net/tunをコンテナに見せてあげる必要がある。 Proxmoxのノードシェルで以下を実行してTUNを有効化する。 pct stop 106 echo "lxc.cgroup2.devices.allow: c 10:200 rwm" >> /etc/pve/lxc/106.conf echo "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file" >> /etc/pve/lxc/106.conf pct start 106 106の部分は自分のLXCのIDに置き換える。IDはpct listで確認できる。 3. LXCにTailscaleを入れる LXCのシェルに入って以下を実行する。 curl -fsSL https://tailscale.com/install.sh | sh tailscale up 認証URLが表示されるのでブラウザで開いてログインする。 認証後、TailscaleのMachines画面からAdGuard HomeのTailscale IPを確認しておく。 ...

April 6, 2026 · 1 min

ArchLinuxのThunarでWalkmanのFSを開く

背景 手持ちのWalkmanをLinux(Arch Linux)環境で活用したいと考えた。 単に音楽を聴くだけでなく、PCのファイルを転送したり、時にはPCの音を高音質で鳴らすオーディオインターフェースとして使いこなすのが目的だ。 ドキュメントを読む限り、最近のデバイスはMTP(Media Transfer Protocol)に対応しており、Linuxでも標準的なツールで扱えるはずだ。 環境 OS: Arch Linux File Manager: Thunar Device: Walkman (MTP/USB DAC対応モデル) Tools: usbutils, gvfs-mtp, libmtp, jmtpfs ThunarでWalkmanのFSが見えない WalkmanをUSBケーブルでPCに接続し、Thunarを開いたがサイドバーには何も表示されない。 まず物理的な接続を確認しようと lsusb を叩いたところ、コマンド自体が入っていなかった。 sudo pacman -S usbutils 改めて確認する。 $ lsusb Bus 001 Device 008: ID 054c:0c2f Sony Corp. Walkman デバイス自体はUSBレベルでは認識されている。fdisk -l にブロックデバイスとして出てこないのはMTPなので当然だ。 原因:MTP用ライブラリが未インストール gvfs-mtp と libmtp が入っていないのが原因だった。 sudo pacman -S gvfs-mtp libmtp # Thunarを再起動して反映 thunar -q これでThunarのサイドバーにWalkmanが表示され、GUIでファイルをコピーできるようになった。 補足:USB DACモードとは 調査中に「DACモードでなければ動かないのか?」と気になって調べたのでここにまとめておく。 USB DACモードとは、デバイスを「ストレージ」としてではなく、**「USBオーディオデバイス」**としてPCに認識させるモードだ。ファイル転送には使えない。 モード PCからの見え方 用途 MTP / MSC ストレージ ファイル転送 USB DAC オーディオデバイス PC音声出力 Walkman側の設定でどちらのモードになっているかは確認しておく必要がある。 ...

April 4, 2026 · 1 min

AI コーディングエージェントを飼い慣らす:最強の「AGENTS.md」の書き方

AI コーディングエージェントを飼い慣らす:最強の「AGENTS.md」の書き方 近年、Gemini、Claude、Aider といった「自律型 AI コーディングエージェント」が開発現場に浸透しつつあります。彼らは指示一つでファイル構成を理解し、コードを書き、テストを実行して修正まで行います。 しかし、エージェントを使い始めた多くのエンジニアが直面するのが、**「AI エージェントの暴走」**です。 型定義を面倒くさがって any を連発する プロジェクト独自のディレクトリ構造を無視して勝手に utils/ を作る vitest --watch などの終了しないコマンドを叩いてフリーズする 指示していないリファクタリングを始めて関係ないファイルを壊す これらの問題を防ぎ、エージェントを「熟練のペアプロ相手」に変える魔法のファイル、それが AGENTS.md です。本記事では、AI エージェントに守らせるべきルールを定義する AGENTS.md の書き方と、その設計思想を徹底解説します。 なぜ AGENTS.md が必要なのか AI エージェントは、人間が数年かけて培った「プロジェクトの阿吽の呼吸」を知りません。 エージェントに与えられるコンテキスト(ファイル内容や履歴)は有限であり、その中で彼らは「最も確率的に正しそうな推論」を行います。その結果、彼らはしばしば**「最も楽な道(=技術的負債を生む道)」**を選んでしまいます。 AGENTS.md をプロジェクトのルートに配置する理由は、**「エージェントの推論空間に強制的な制約(ガードレール)を設けるため」**です。 なぜその設計にしたか:外部メモリとしての役割 エージェントは毎回ゼロベースで思考するわけではありませんが、多くのファイルを見すぎることで逆に重要なルールを見失う(ロスト・イン・ザ・ミドル現象)ことがあります。AGENTS.md という明確なルールブックを定義し、エージェントの起動時やタスク開始時に必ず読み込ませることで、思考のブレを最小限に抑えることができます。 AGENTS.md に書くべき内容の 4 つの分類 効果的な AGENTS.md は、以下の 4 つのセクションで構成するのがベストプラティスです。 禁止事項 (Prohibitions): 致命的なエラーや環境のハングを防ぐ 命名規則・コーディング基準 (Standards): コードの品質を一定に保つ アーキテクチャ原則 (Architecture): システムの整合性を維持する テスト・検証方針 (Testing): 修正の正しさを担保する それぞれのセクションについて、具体的な記述例を見ていきましょう。 1. 禁止事項 (Prohibitions) エージェントが最もやりがちな「環境破壊」を防ぐための最重要セクションです。 ルール 理由(なぜその設計にするか) インタラクティブコマンドの禁止 npm init や git commit (メッセージ入力待ち) など、ユーザー入力を待つコマンドは CLI エージェントをフリーズさせます。 Watch Mode の禁止 vitest --watch 等はプロセスが終了しないため、エージェントが「完了」を検知できなくなります。 any 型の原則禁止 AI は型の整合性を取るのが面倒になると any で逃げようとします。これは長期的な保守性を著しく低下させます。 勝手な依存関係の追加禁止 package.json を書き換えて新しいライブラリを入れる行為は、セキュリティやビルドサイズに影響するため、人間の許可を必須にします。 実例コード ## 禁止事項 - **コマンド実行**: - `vi`, `nano`, `top` などのインタラクティブなコマンドは実行しない。 - `npm start`, `vitest --watch` などの終了しないプロセスは背景実行 (`&`) するか、単発実行モードを使用すること。 - **TypeScript**: - `any` 型の使用は厳禁。どうしても必要な場合はコメントで理由を明記すること。 - **Git**: - ユーザーの明示的な指示なしに `git commit` や `git push` を行わない。 2. 命名規則・コーディング基準 (Standards) プロジェクトの「見た目」と「一貫性」を守るためのルールです。ここを疎かにすると、エージェントは自分の得意なスタイル(多くの場合、学習データで最も多いスタイル)で書き始めてしまいます。 ...

March 28, 2026 · 2 min

Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック

Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック 1. 概要 Web ゲーム開発、特にローグライクやタクティカル RPG を開発する際、避けて通れないのが「マップの描画」だ。通常、これらは「タイルセット」と呼ばれる画像ファイルを読み込んで描画するが、開発の初期段階や、あえて外部アセットに頼りたくない場合、Canvas 2D API を使ってプログラムで直接タイルを描画する「手書き(プログラマティック描画)」の手法が非常に強力な武器になる。 本記事では、HTML5 Canvas の fillRect や beginPath などの基本命令のみを使い、擬似的なピクセルアート風のタイルを描画するテクニックを解説する。画像を用意する手間を省きつつ、動的に色や形状を変更できる柔軟な描画システムを構築しよう。 2. タイル描画の基本構造 まずは、どのようなタイルでも共通して利用できる描画の入り口を作る。ピクセル座標ではなく、タイル座標(x, y)とタイルサイズ(tileSize)を受け取る設計にすることで、グリッドベースのシステムと統合しやすくなる。 /** * タイルを描画するメイン関数 * @param {CanvasRenderingContext2D} ctx - Canvasコンテキスト * @param {string} tileType - タイルの種類 ('wall', 'floor', 'grass', etc.) * @param {number} x - タイルのX座標(グリッド単位) * @param {number} y - タイルのY座標(グリッド単位) * @param {number} tileSize - 1タイルのピクセルサイズ * @param {string} fieldType - フィールドの種類 ('meadow', 'forest', 'mountain') */ function drawTile(ctx, tileType, x, y, tileSize, fieldType = 'meadow') { const px = x * tileSize; const py = y * tileSize; ctx.save(); ctx.translate(px, py); switch (tileType) { case 'floor': drawFloor(ctx, tileSize, fieldType); break; case 'wall': drawWall(ctx, tileSize, fieldType); break; case 'object_grass': drawFloor(ctx, tileSize, fieldType); drawGrass(ctx, tileSize); break; case 'object_tree': drawFloor(ctx, tileSize, fieldType); drawTree(ctx, tileSize); break; case 'stairs_down': drawFloor(ctx, tileSize, fieldType); drawStairs(ctx, tileSize, false); break; default: ctx.fillStyle = '#333'; ctx.fillRect(0, 0, tileSize, tileSize); } ctx.restore(); } この設計のポイントは、ctx.translate を使ってタイルの左上を原点 (0, 0) に固定することだ。これにより、各タイルの描画ロジック内で座標計算を簡略化できる。 ...

March 28, 2026 · 4 min