マインスイーパーの RESTful な API サーバーをつくる


以前、Vue.js を使ってマインスイーパーを実装しましたという記事を書きました。そちらでは、ビジネスロジックは Vue に依存しないように作っていたので、ふと思い立って今回はそれをサーバーサイドに移植してみました。

本記事では、実装の細かいところは書ききれないので設計のポイントになる部分を説明します。

作ったもの

API サーバ

API サーバに対応したクライアントプログラム

環境

node 14.16.0
express 4.16.4
MySQL 8.0

1. 基本方針

Vue.js で作成したときの Model はそのまま再利用します。前回の記事で書きましたが、ビジネスロジックは Vue.js に依存しない POJO なクラス群で作成しているため、ほとんどそのまま移植しています。

全体の構成は、大雑把に次のような構成。

説明
Controller HTTPリクエストのハンドリング。HTTPに関する知識はここだけが持つ。
Repository DBとのやりとり。テーブルレイアウトに関する知識はここだけが持つ。
Model Vue で作ったマインスイーパーから移植した部分。ビジネスロジック。
Controller や Repository には依存しない。

2. データ設計

基本となるデータ構造は Cell, Game の2つ。
データベース上も2つのテーブルを作成。

Cell

一つのマスを表すデータ。
セルの位置座標 (x, y) と、画面表示に必要な情報を保持。

Cell
{
  "x": 0,
  "y": 0,
  "count": 2,      // 周囲のセルの地雷数
  "isMine": false, // 地雷があるか?
  "isOpen": false, // 開かれているか?
  "isFlag": false  // フラグが立てられているか?
}

Game

ゲーム全体を表すデータ。
盤面の広さや地雷数などの設定値、ゲームのステータスや開始・終了時刻などを持ちます。

Game
{
  "id": 999,      // ID
  "width": 9,     // 盤面の横幅
  "height": 9,    // 盤面の高さ
  "numMines": 10, // 地雷数
  "status": "PLAY", // ステータス
  "startTime": "2021-06-27T08:22:34.470Z", // 開始日時
  "endTime": null, // 終了日時
  "cells": [ /* 盤面上すべてのセルが入った配列 */ ]
}

3. URL設計

ボツ案

まずは、はじめに考えたもの。
データ構造をそのままURLにした。

HTTP メソッド パス 説明
POST /api/games Game の作成
GET /api/games/{id} Game の取得
GET /api/games/{gameId}/cells Cell 一覧の取得
PATCH /api/games/{gameId}/cells/{id} Cell の更新

イマイチなところ

  • なんか REST っぽくない
  • セルを開く、フラグを立てる、フラグを外す、という操作がすべて Cell に対する PATCH で行われている
  • ゲーム上あり得ないリクエストをどう扱うのか悩むところが増える

このままでも機能的には実現できるんですが、うまいやり方はないかと試行錯誤して次の形に落ち着きました。

最終的なURL設計

ポイントは「セルを開く」という操作を「『開いたセル』を作る」として扱うように変更したことです。これによって、HTTPメソッドとURLの組み合わせで、何に対するどういう操作なのか、を表現できるようになりました。

HTTP メソッド パス 説明
POST /api/games Game の作成
GET /api/games/{id} Game の取得
POST /api/games/{gameId}/open-cells Cell を開く
POST /api/games/{gameId}/flags フラグを立てる
DELETE /api/games/{gameId}/flags/{id} フラグを外す

4. チート対策

ゲームのAPIサーバーなので一応チート対策として、ゲームが終了するまではどのセルに地雷がセットされているのか、という情報をクライアントに渡さないようにしています。

Express のドキュメントによると res.json() メソッドは内部で JSON.stringify() を使っています1。また、オブジェクトに toJSON メソッドを実装すると JSON.stringify() の挙動を変更することが可能です2。このことを利用して、ステータスがプレイ中の場合は isMine フラグを表示しない、などの制御を入れています。

toJSONの使用例
const obj = {
  value: 0,
  toJSON () {
    return {
      value: this.value,
      text: this.value % 2 === 0 ? 'even' : 'odd'
    }
  }
}

obj.value = 1
JSON.stringify(obj) // "{\"value\":2,\"text\":\"odd\"}"

obj.value = 2
JSON.stringify(obj) // "{\"value\":2,\"text\":\"even\"}"

さいごに

きっかけは「AWSの勉強をするために簡単な Web アプリケーションを作ってみよう」ということだったんですが、
やってみたら色々学ぶことが多く非常に楽しかったです。

ただやっぱり型が無いのは辛いですね…次は TypeScript に手を出してみようかな…