1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「チェス×カードゲーム」をAIと議論して3日でブラウザ対戦ゲームにした話【Chessive】

1
Posted at

01-title.jpg

カードで動かし、盤で読み合う。 そんなカードゲーム×ボードゲーム「Chessive(チェッシブ)」の体験版を公開しました。ブラウザでそのまま遊べます(無料・インストール不要・スマホ対応)。

👉 https://gunghi.kn-clinic.org/chessive/

まだ体験版の段階です。 ルールのバランス、見た目(カードのイラストは仮のドット絵)、遊べるモードは、遊んでくれた人の感想をもとに調整を続けています。気になった点は、ゲーム内の「感想を送る」から教えてもらえるとうれしいです。

10-reveal.gif

この記事では、

  1. どうやってこのゲームが生まれたか(ルールを GPT と議論して、何度も却下して決めた話)
  2. このゲームの面白ポイント
  3. 実装の工夫(同時解決アルゴリズム、2手読みCPU、サーバー権威のオンライン対戦、素材ゼロのドット絵とチップチューン、決定的に書き出すトレーラー…)
  4. これからゲームを作る人に役立ちそうな知見

を、スクショ多めでまとめます。

ルールの議論は GPT(Codex)と、実装は Claude Code と一緒にやりました。アイデアから公開まで、だいたい3日です。


1. Chessive ってどんなゲーム?

ルールは30秒で説明できます。

  • 盤は 5×5。駒は 王・飛車・角・歩×2(将棋でおなじみの動き)。
  • 手札のカードには 「動かせる駒」 が書いてある。どこへ動かすかは自分で決める。
  • お互いカードを1枚選んで、同時に公開。駒がいっしょに動く。
  • 相手の 王のライフ(♥3つ)を0 にしたら勝ち。

02-select.jpg

03-reveal.jpg

相手も同時に動くので、狙った駒が逃げる(空振り) こともあるし、同じマスに入ろうとしてぶつかる こともあります。ぶつかったら、ラウンドごとに交代する ★優先権 を持っている方が勝ちます。

そして、デッキに1枚だけ入っている切り札が 【桂馬】。王以外の駒を、その一手だけ将棋の桂馬の動きで 跳ばせる カードです。

12-knight.gif

スマホでもそのまま遊べます。

05-mobile.jpg


2. どうやって生まれたか:GPT とのルール討論

最初に考えていたのは、こんな漠然としたアイデアでした。

チェスみたいなボードゲームとカードゲームを組み合わせた新感覚ゲーム。駒の動かし方をカードで決め、相手を詰ませる。
でも当然普通のチェスだと運ゲーになっちゃうので、単なるチェス的な詰ませ方にもう一段階必要だと思っている

これを GPT に投げて、具体化してもらいました。ここからが長かったです。

案1:王手を「予告」するゲーム → 「爽快感がない」

GPT の最初の案は、王を取る一手だけは事前に予告が必要 というものでした。隠していたカードで奇襲しても、勝利を決める一手は見えている。理屈としてはきれいです。

でも、私が大事にしたかったのは 爽快感 でした。

単に攻防を予告しあうだけでは派手さがなく、ゲームとしての楽しさは作れないかなと

案2:押し出し・受け流しのコンボ → 「じゃんけんと一緒」

次に出てきたのが、突進・受け流し・踏ん張り のようなカードで駒をぶつけ合い、衝突から連鎖を起こす案です。私も「王だけライフ制にする」「お互い同時に手を出す」「相手の手でコンボが生まれる」とアイデアを足して、ポケモンのような同時ターンバトル の方向に進みました。

ところが、GPT に画像つきで説明してもらった瞬間に違和感がはっきりしました。

うーん、却下かな
じゃんけんと一緒すぎて、じゃんけんでいいとなる
残したいのは、チェスや将棋にある、駒特有の動き、というところ

カードに「受け流し」のような 相性 が書いてあると、結局「相手が突進するか、しないか」の当て合いになります。盤面を読む意味が薄れる んです。

転換点:カードには「動かす駒」だけ書く

ここで方針を自分で決め直しました。

カードは動きが書いてあるのではなく、動かす駒だけ書いてあり、どこに動かすかはプレイヤーが決める、にした方がいかせるかも

  • 駒の動きは固定(飛車は縦横、角は斜め)。だから盤面から相手の狙いが読める。
  • 手札で「今動かせる駒」が変わる。だから毎試合同じにならない。
  • 同時に動くときは 優先権 で決着。動かない敵は優先権に関係なく取れる。
  • 駒の補充・位置交換・ドローのカードで、作戦の幅を出す。

GPT はこれを 「手札で動かせる駒が変わる、同時行動の将棋」 とまとめてくれました。

決め手:桂馬だけをカードにする

最後のひらめきがこれです。

あえて桂馬だけカードにして、相手の駒が突然桂馬の動き方をしてくる(ただしデッキに1枚までとか)
ここで桂馬使うかーみたいな

普段は駒の動きが決まっているからこそ、それを一度だけ破る切り札が映える。 ここでゲームの核が固まりました。GPT の言葉を借りると 「盤面で考えさせて、切り札で驚かせる」 です。

この議論から学んだこと

  • AI はアイデアを大量に出してくれるが、「何を残したいか」を決めるのは人間。 案を却下するたびに、自分のやりたいことがはっきりした。
  • 画像で説明してもらうと、違和感に気づける。 文章では良さそうに見えた案が、絵にした瞬間「じゃんけんだ」と分かった。
  • 最後に 草案(idea.md) に落としてもらった。盤面サイズ、デッキ構成、未確定事項、「試遊でこういう問題が出たら、まずこれを変える」という調整表 まで入っている。これが実装の設計書になった。

3. 草案から公開まで(Claude Code との3日間)

草案の Markdown を Claude Code に渡して、「試遊用に実装できる?」から始めました。

段階 やったこと
1日目 ルールエンジン、CPU、ドット絵、BGM。CPU 同士の自動対戦でバランスを確認
2日目 遊んで気になった点を直す(駒の見た目、演出、ルール調整)。オンライン対戦サーバー、既存サーバーへの公開
3日目 チュートリアル、攻撃演出、スマホ対応、SEO、アイコン、紹介動画

流れはずっと 「遊ぶ → 気になったことを一言で伝える → すぐ直る → また遊ぶ」 の繰り返しでした。「飛車と角が似すぎ」「王冠を中くらいに」「到達って名前ダサい」くらいの粒度で伝えて、数分で反映されます。この速さのおかげで、試遊の熱が冷めないうちに直せた のが大きかったです。

04-tutorial.jpg


4. 面白ポイント

同時公開の読み合い

相手の駒の動きは決まっているので、「この角が動くなら、ここが空く」「動かないなら、こっちで取れる」 と、複数の可能性に対して得をする配置を考えられます。じゃんけんではなく、盤面に根拠のある読み合いです。

王を討つ一撃

王は取られない代わりに、攻撃されるとライフが1減ります。一度の読み負けでは終わらないけれど、決まったときの手応えは大きい。ここは演出もいちばん派手にしました。

11-king-hit.gif

捨て札から相手を読む

使ったカードは公開されます。「相手はもう桂馬を使った」「角のカードはまだ3枚残っている」が分かるので、カードゲームらしい読みも効きます。

06-discard.jpg

歩の「突破」

歩が相手の一番奥の列に着くと「突破」。2回で相手の王に1ダメージです。王を直接狙うだけでなく、歩で押し込む 勝ち筋もあります。


5. 実装の工夫

ここからが Qiita らしいところです。フレームワークもビルドも使わず、素の JavaScript だけ で作っています(全体で約5,300行)。

index.html / rules.html / feedback.html
js/rules.js     ルールエンジン(DOM なし)
js/ai.js        CPU(やさしい/ふつう/つよい)
js/ui.js        画面・入力・演出
js/sprites.js   ドット絵(文字列から描く)
js/audio.js     BGM・効果音(WebAudio で合成)
js/net.js       オンライン対戦・計測・アンケート
server/         対戦サーバー(Node.js + ws)
tests/          ルールのテストと CPU 同士の自動対戦
trailer/        紹介動画を書き出すページ

5-1. ルールエンジンは DOM から切り離して、どこでも同じコードを使う

rules.js は画面のことを一切知りません。ブラウザでは window.Rules、Node.js では require で読めるようにしてあります。

(function (root) {
  // …ルール本体…
  root.Rules = api;
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
})(typeof window !== 'undefined' ? window : globalThis);

この1ファイルを、画面・CPU・自動テスト・オンライン対戦サーバー の4か所で共有しています。

  • オンライン対戦では、サーバーがこのルールで勝敗を計算するので、画面とサーバーで判定がずれない。
  • 後で説明するバランス調整のシミュレーションも、ブラウザなしで回せる。

ゲームのルールは、最初から「画面なしで動く」形にしておく。 これが一番効いた設計判断でした。

5-2. 同時解決は「結果が変わらなくなるまで」繰り返す

同時行動のゲームで一番やっかいなのが、解決の順番 です。例えばこんな場面があります。

  • 自分の飛車が、相手の王を攻撃する(王はライフ-1、飛車は元の位置に戻る)
  • 同時に、相手の駒が「飛車がいた場所」を狙っている

飛車は動いたのか、動いていないのか? Chessive では 「王を攻撃して戻った駒は、動かなかった駒として扱う」 と決めました。すると、その場所を狙った相手の駒は「動かない敵を攻撃した」ことになり、飛車を取れます。

ただ、ある駒が「動かなかった」ことになると、別の駒の判定も変わる ことがあります。そこで、判定が変わらなくなるまでループしています。

// 止まっている敵の王を攻撃した駒、止まっている味方に塞がれた駒は「動かなかった」扱い。
// それにより別の駒の判定が変わるので、安定するまで繰り返す。
let changed = true;
while (changed) {
  changed = false;
  for (const m of [...active]) {
    const occ = pieceAt(s, m.to.x, m.to.y);
    if (!occ || gone.has(occ.id) || isActive(occ)) continue;
    if (occ.owner === m.owner) {
      m.outcome = 'blocked';      // 味方に塞がれた
    } else if (occ.type === 'king') {
      m.outcome = 'bounce';       // 王を攻撃して戻る
      m.hitKing = occ.owner;
    } else continue;
    active.delete(m);             // 「動いている駒」から外す
    changed = true;
  }
}

この方式は、「号令」(歩が全員同時に1マス進む)のように 1人が複数の駒を同時に動かすカード を足したときも、そのまま使えました。

テストも、こういう「ややこしい場面」を1つずつ書いています。

// 王を攻撃して戻った駒は「止まっている」→ その元の位置を狙った敵に取られる
{ const s = blank(); put(s,0,'king','A5'); const k1 = put(s,1,'king','C1');
  const a = put(s,0,'rook','C3'); const b = put(s,1,'rook','E3');
  R.resolveRound(s, [mv(s,a,'C1'), mv(s,b,'C3')]);
  assert.strictEqual(s.players[1].life, 2); assert(!R.getPiece(s,a.id)); assert.strictEqual(at(b),'C3'); }

5-3. バランスは「CPU 同士に数百試合させて」数字で決める

ルールを変えるたびに、CPU 同士で数百試合を自動で回して比べました。node tests/sim.js 300 old new pair tally のように、設定の名前を並べるだけで比較表が出ます。

例えば、歩の突破ルールは3案を比べました。

ルール 平均ラウンド 突破が王ダメージに占める割合 40ラウンドの上限まで
1体着いたら1ダメージ 15 37%(強すぎ) 0%
2体そろったら1ダメージ 22 5%(弱すぎ) 13%
突破2回ごとに1ダメージ(採用) 20.5 14% 4%

「2体そろったら」は直感的には良さそうでしたが、数字を見ると 先に着いた歩が相手の王の隣で待たされて、ほぼ必ず取られる ことが分かりました。

一度は追加した「突撃(飛車・角が距離無制限)」も、遊んで「初手で飛車が角を取れるのは理不尽」と感じたので、数字で確かめてから外しました。

デッキ 8ラウンド以内に決着 先手・後手の勝ち数
突撃あり 10% 148 対 124
突撃を外して角を1枚戻す 3% 139 対 139

遊んだ人の「なんか理不尽」を、数字で確かめてから直す。 この往復が、短期間でも納得感のある調整につながりました。

5-4. CPU の「2手読み」は、相手の手札を見ずに「相手がやりそうな手」を重み付けする

CPU は3段階です。「ふつう」は、相手が動かない前提で1手先を読みます。「つよい」は、相手の行動も予測 します。

ただし、相手の手札をのぞき見するのはズルです。そこで、相手の駒が盤面上でできる手をすべて挙げて、相手目線で評価し、良さそうな手ほど起きやすいとみなします(softmax で重み付け)。

function opponentModel(s, o, L) {
  const op = 1 - o;
  const acts = [{ kind: 'pass', owner: op }];
  for (const p of s.pieces) {
    if (p.owner !== op) continue;
    for (const to of R.normalMoves(s, p)) acts.push({ kind: 'move', owner: op, pieceId: p.id, from: { x: p.x, y: p.y }, to });
    // 桂馬がまだ除外されていなければ、桂馬の跳びも候補に入れる …
  }
  // 相手目線の1手読みで評価し、上位 K 手を softmax で重み付けする
  const scored = acts.map((a) => ({ a, v: evaluate(simulate(s, op, a, pass), op) })).sort((x, y) => y.v - x.v);
  const top = scored.slice(0, L.K);
  const ws = top.map((x) => Math.exp((x.v - top[0].v) / L.T) + (x.a.kind === 'pass' ? L.passW : 0));
  const sum = ws.reduce((a, b) => a + b, 0);
  return top.map((x, k) => ({ a: x.a, w: ws[k] / sum }));
}

ここで面白かったのは、最初の「つよい」は「ふつう」に負けた(79勝102敗)ことです。相手の予測を鋭くしすぎると、「相手は最善手を打ってくるはず」と考えて守りに偏ってしまう。予測を広めにし(上位16手、温度高め、相手が様子見する可能性も残す)、さらに 有力な10手だけは自分の次の手まで読む ようにして、ようやく 189勝84敗 になりました。

5-5. オンライン対戦は「サーバーが審判」+「後手には盤を180度回して送る」

オンライン対戦(ランダムマッチと部屋コード対戦)は、Node.js と ws で作っています。

07-room.jpg

設計のポイントは2つです。

1つ目:勝敗の計算はすべてサーバー。 クライアントから来た行動は、そのまま使いません。サーバー側でその時点の 合法手の一覧を作り、その中に一致するものがあるか を確かめて、サーバーが作った正式な行動に置き換えます。

return R.legalActionsForCard(s, v, card).find((l) => {
  if (l.kind !== a.kind) return false;
  if (l.kind === 'move' || l.kind === 'knight') return l.pieceId === a.pieceId && same(l.to, a.to);
  if (l.kind === 'reinforce') return same(l.to, a.to);
  if (l.kind === 'swap') return (l.a === a.a && l.b === a.b) || (l.a === a.b && l.b === a.a);
  return true;
}) || null;

2つ目:プレイヤーごとに「見え方」を作って送る。 相手の手札と山札の中身は、枚数だけ残して伏せます。さらに後手のプレイヤーには 盤を180度回転させ、プレイヤー番号も入れ替えて 送ります。

function viewState(canon, v) {
  const s = clone(canon);
  s.players.forEach((P, o) => {
    P.deck = hidden(P.deck.length, -1000 * (o + 1));         // 山札の中身は誰にも見せない
    if (o !== v) P.hand = hidden(P.hand.length, -100 * (o + 1)); // 相手の手札は枚数だけ
  });
  if (v === 1) {                                              // 後手には盤を回して「自分=0番」に見せる
    s.players.reverse();
    s.priority ^= 1;
    for (const p of s.pieces) { const f = flipPos(s, p); p.x = f.x; p.y = f.y; p.owner ^= 1; }
  }
  return s;
}

こうすると、クライアントは常に「自分が下側のプレイヤー0」として動けばいい ので、CPU 戦の画面コードをほぼそのまま使い回せました。マス名(A1 など)だけは、どちらから見ても同じになるように変換しています。

切断しても2分以内なら自動で復帰でき、戻らなければ不戦敗です。2つの接続を使って「部屋対戦 → 再戦 → ランダムマッチ → 切断と復帰」を最後まで通す自動テストも用意しました。テストでは、相手の手札が見えていないこと と 2人の盤面が180度回転の関係で一致していること も毎ラウンド確かめています。

5-6. 素材ファイルゼロ:ドット絵は文字列、BGM は WebAudio で合成

イラストは後から差し替える前提で、仮の素材を全部コードで作りました。

ドット絵は、16×16 の文字列で描いています。例えば飛車のミニシルクハットと羽はこうです(o=輪郭、w=白、p=ピンク)。

rook: {
  0: '.......oooo.....',
  1: '.......owwo.....',
  2: '.......owwo.....',
  3: '.....poppppo....',   // ピンクのリボン
  4: '....owwwwwwwo...',   // 帽子のつば
  9: '.oo..........oo.',   // ここから羽
  10: 'owwo........owwo',
  // …
},

共通の体に、駒ごとの「頭の飾り」を上書きして作ります。「飛車と角が似すぎ」「王冠を中くらいに」といった見た目の調整も、この文字列を数行書き換えるだけでした。

BGM と効果音は、音声ファイルを使わず WebAudio でその場で合成 しています。ファミコン風の音色は、デューティ比つきの矩形波を PeriodicWave で作ります(パルス波のフーリエ係数)。

function pulse(duty) {
  const n = 32, re = new Float32Array(n), im = new Float32Array(n);
  for (let k = 1; k < n; k++) {
    const a = 2 / (k * Math.PI) * Math.sin(k * Math.PI * duty);
    re[k] = a * Math.cos(k * Math.PI * duty);
    im[k] = a * Math.sin(k * Math.PI * duty);
  }
  return ctx.createPeriodicWave(re, im);
}

楽譜も文字列です(1トークン=8分音符、- は伸ばす)。

'D5 - G5 - B5 - A5 G5', 'F#5 - A5 - D5 - - -', 'E5 - G5 B5 A5 - G5 E5', 'G5 - - E5 C5 - - -',

王のライフが1になると、同じ曲のまま テンポを上げ、キーを上げ、ドラムを激しくする「盛り上がりモード」 に、次の小節の頭から切り替わります。自分がピンチのときは、同じメロディを短調に読み替えて(シ→シ♭、ミ→ミ♭)不穏にしています。曲を何曲も作らなくても、場面に合わせて表情を変えられるのが合成の強みです。

5-7. 気持ちよさの演出:時間で進むエフェクト+ヒットストップ+ズーム

攻撃の演出は、「溜め → 突進 → 命中 → 吹き飛ばし」 の4段階です。王への一撃だけは、命中の瞬間に 0.13秒止める(ヒットストップ) →赤い×斬り→盤を王に寄せるズーム、と特別扱いしています。

エフェクトは「開始時刻・長さ・描く関数」を配列に積んでおき、描画のたびに経過時間から描くだけです。

function addFx(dur, draw, delay) { FX.push({ start: performance.now() + (delay || 0), dur, draw }); }
function drawFx() {
  const t = performance.now();
  for (let i = FX.length - 1; i >= 0; i--) {
    const f = FX[i], p = (t - f.start) / f.dur;
    if (p >= 1) { FX.splice(i, 1); continue; }
    if (p >= 0) { ctx.save(); f.draw(p); ctx.restore(); }
  }
}

ちなみに、アニメーションは requestAnimationFrame ではなくタイマーで進めています。ブラウザのタブが裏にあると rAF が止まり、解決が永遠に終わらない というバグを自動テスト中に踏んだためです。

5-8. 紹介動画は「時刻 t だけから1コマを描く」関数で決定的に書き出す

Steam などに載せる紹介動画(32秒・1920×1080)も、ゲームと同じドット絵と演出で作りました。画面録画ではなく、render(t) が時刻だけから1コマを描く 作りにして、1コマずつ PNG に書き出しています。

async function exportAll() {
  await save('audio.wav', wav(await Sound.renderOffline(AUDIO)));  // BGM と効果音を時刻どおりに並べて書き出す
  for (let i = 0; i < DUR * FPS; i++) {
    render(i / FPS);
    const blob = await new Promise((r) => cv.toBlob(r, 'image/png'));
    await save(`frame_${String(i).padStart(5, '0')}.png`, blob);
  }
}

あとは ffmpeg でまとめるだけです。

ffmpeg -framerate 30 -i out/frame_%05d.png -i out/audio.wav -c:v libx264 -crf 16 -pix_fmt yuv420p -c:a aac -shortest chessive-trailer.mp4
  • 画面録画と違って コマ落ちしない。何度書き出しても同じ映像になる。
  • 音は OfflineAudioContext で、実時間より速く書き出せる。ゲーム本体の効果音関数を、「今」ではなく指定した時刻に鳴らせる ように少し改造しただけで使い回せた。
  • 粒子の散り方も、時刻とシードから計算するので毎回同じ。

5-9. スマホ対応の小ネタ

  • 手札は横スクロールさせない。 8枚でも1列に収まるよう、幅が足りない分だけカードを重ねます。重ねる量は「足りない幅 ÷ (枚数−1)」で計算して、--ov という CSS 変数で margin-left に入れるだけです。
  • 入口で見せ方を変える。 長い文章はスマホのモーダルでは窮屈なので、「あそびかた」と「感想を送る」は独立したページも作りました。タイトル画面からはそのページへ移動し、プレイ中はモーダルでさっと開く、と入口で使い分けています。独立ページは検索にも引っかかるので、SEO の面でも得です。
  • 画面の重なり順は、実際に通しで触って確かめる。 この記事のスクショを撮っていて、タイトル画面から「部屋を作る」と、部屋コードの画面がタイトル画面の裏に隠れる バグに気づきました(直しました)。自動テストではロジックは確かめられても、「見えているか」までは分かりません。

5-10. 既存のサーバーに「相乗り」して公開する

公開先は、別に作っていた将棋系ゲーム(軍儀)のサーバーです。同じドメインの /chessive/ 以下で動かしています。

  • Chessive は 別のプロセス・別のユーザー で動かし、nginx が /chessive/ だけを振り分ける。片方が落ちても、もう片方に影響しない。
  • アプリは環境変数 BASE_PATH=/chessive でサブパスに対応。クライアントの API と WebSocket の接続先は、ページの場所からの相対で決める。
  • インストールスクリプトは、既存の nginx 設定に1行足す前に控えを取り、nginx -t が通らなければ全部元に戻す。既存サービスを壊さないことを最優先にした。
  • 手元の PowerShell から deploy.ps1 を1回実行するだけで、ファイルを送って入れ直せる。

6. これからゲームを作る人へ:この開発で得た知見

ルール作り

  • AI の案は、どんどん却下していい。 却下の理由(「爽快感がない」「じゃんけんになる」)を言葉にするたびに、自分が残したいものがはっきりする。
  • 「残したい体験」を最初に書く。 草案には「大切にしたい体験」と「採用しない方向」の両方を書いた。迷ったときの判断基準になる。
  • 調整表を最初から作っておく。 「王がすぐ倒れたら、ライフを3→4」のような表は、そのままゲームの設定項目になり、そのままシミュレーションの比較条件になった。
  • 未確定のルールは「仮決め」として明記する。 実装を止めずに進められ、遊んでから見直せる。

実装

  • ルールは画面から切り離す。 テスト、CPU、シミュレーション、オンライン対戦の土台になる。
  • 数字で議論する。 「強すぎる気がする」を、数百試合の比較表で確かめてから直す。
  • 素材は後から差し替えられるようにして、最初は全部コードで作る。 見た目と音を数行の修正で直せるので、試遊のサイクルが速い。イラストの指示書(ファイル名・サイズ・キャラクター案)も先に書いておくと、後で外注や画像生成に渡しやすい。
  • 演出は面白さそのもの。 ヒットストップ、ズーム、盛り上がる BGM を入れただけで、同じルールでも「決まった!」の手応えがまるで変わった。

公開

  • 試遊の仕組みを最初から入れる。 終了時の記録コピー、アンケート、日ごとのアクセス数の管理ページを入れておくと、身近な人以外の反応も拾える。
  • 共有されたときの見た目を整える。 アイコン、SNS 用の画像(OGP)、題名と説明文。検索より先に、リンクを貼られたときの見え方が効く。
    09-og.png

08-icon.png


おわりに

「チェスとカードゲームを混ぜたら面白そう」という一言から、AI と議論して、却下して、遊んで、数字で確かめて、3日で人に遊んでもらえる形になりました。一番大事だったのは、AI に任せきりにせず、「自分がこのゲームで何を感じたいか」を言葉にし続けたこと だと思います。

まだ体験版なので、ここからが本番です。ぜひ遊んでみて、感想を送ってもらえるとうれしいです!

👉 https://gunghi.kn-clinic.org/chessive/

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?