🏓

手番のデータ構造と考察

2022/09/17に公開

自作のオンラインゲームで試行錯誤の末に捻り出した手番に関するノウハウの備忘録です

仮定する状況

  • 1台の卓球台がある
    • リバーシでもいいけど卓球の方がイメージしやすい
  • abcd の4人がそれで遊びたかった
  • 黒と白のチームに分かれた
  • 1セット目は黒からサーブを打つ
    • なので必ず黒白黒白……の順になる
  • 卓球のダブルスのルール通り、順番に打つ番がくる
    • テニスのように球が近い人が打つのはだめ
  • データ構造の1つ目はシンプルに abcd となっている
    • この場合は 黒a 白b 黒c 白d の順で打つ
  • データ構造の2つ目はチーム毎に分かれていて a bcd となっている
    • bcd は仲良しなので一緒の白チームになった
    • つまり1人対3人
    • この場合は 黒a 白b 黒a 白c 黒a 白d の順になる
  • そのうち移動が大変なので一人2回打ったら交代するとした
    • そうなると各データ構造での順はこうなる (チーム名省略)
      • データ構造1: ababcdcd
      • データ構造2: ababacacadad
  • さらに2セット目は黒から始まっていたサーブが白からになる
    • ので2つごとに入れ替わった順になる
      • データ構造1: babadcdc
      • データ構造2: babacacadada

そんな状況で指定ターンのメンバーを O(1) で求めたい

主な変数の意味

意味
g メンバーの配列 or メンバーの配列(チーム)の配列
t turn。t番目のメンバーを知りたい。0から始まる。負の値も考慮する
n 一人n回毎に交代する。基本は1だけど以下は2としている
s サーブを打つ側。0:黒 1:白 で以下は白から始まる例としている
c color。t番目のメンバーの所属チームの色がわかる 0:黒 1:白

ハードコーディングしている2は卓球が二手に分かれるルールを意味している。
もし卓球が3手に分かれ3方向から打ち合うゲームであれば3になる。

データ構造1: メンバーが順番に並ぶ

g = [:a, :b, :c, :d]
n = 2
s = 1
f = -> t {
  b = t / (2 * n) * 2
  c = (t + s) % 2
  i = b + c
  [t, b, c, i, g[i % g.size]]
}
f[-1]  # => [-1, -2, 0, -2, :c]
f[0]   # => [0, 0, 1, 1, :b]
f[1]   # => [1, 0, 0, 0, :a]
f[2]   # => [2, 0, 1, 1, :b]
f[3]   # => [3, 0, 0, 0, :a]
f[4]   # => [4, 2, 1, 3, :d]
f[5]   # => [5, 2, 0, 2, :c]
f[6]   # => [6, 2, 1, 3, :d]
f[7]   # => [7, 2, 0, 2, :c]
# t = 7 と -1 は同じ角度
  • メリット
    • データ構造がシンプルでわかりやすい
  • デメリット
    • ひと目、誰がどちらのチームかわかりにくい
    • 奇数人数では遊びにくい
      • たまたま a b c d の4人だったからよかった
      • もし1人や3人や5人だったら一周するたびに所属チームが逆になってしまう
        • 仮に a b c の3人で始めた場合はこうなる
          1. a と b が向かい合い、c は a の後ろで待機する
          2. a がサーブを打った直後走って対面の b の後ろにまわる
          3. b が返球した直後走って対面の c の後ろにまわる
          4. c が返球した直後走って対面の a の後ろにまわる
          5. a が返球した直後走って対面の b の後ろにまわる
        • かなりカオスな状況になる
      • それはそれでおもしろいとの見方もあるが勝敗を重視すると誰が勝ちなのかはっきりしなくなる
    • 偶数人数であっても1人対3人な分け方はできない
  • 向いているケース
    • メンバー数が偶数と決まっている場合
    • 自分 vs 自分をしたい場合
    • パーティ vs 討伐モンスター
      • a b c d の順でモンスターに攻撃する
      • 3人(a b c)であっても矛盾は生じない
  • 対応するUI
    • 配列が1つあればよい
    • 各要素(メンバー)に上下移動ボタンをつけて順番を調整できるようにしておく
      • ただこれは操作が直感的でない
      • 実際に利用者から改善の要望が来た
    • 上下移動ボタンではなくDnDで入れ替えれるとなおよい
      • しかし逆に入れ替え方が分からなくなる人がいたため本当にUIは難しい
  • データ構造を生かす案 (おすすめ)
    • 奇数かつ先後逆になった場合 abcbaacc と奇妙な順になる
      • 複数人いるのに一人で連続で打つのは不自然に見える
      • これではプレイヤーもいつ自分の番がくるのか予測しづらい
    • このデータ構造は「順番に並んでいる」のが利点なのでそれを生かして「先後を無視する」のが良いかもしれない
      • 具体的にはサーブを打つ側を表す変数 s を無視する
    • そうすると黒白どちらから開始であっても上から順に abc の繰り返しになる

データ構造2: チーム別で並ぶ

g = [[:a], [:b, :c, :d]]
n = 2
s = 1
f = -> t {
  c = (t + s) % g.size
  i = t / (g.size * n)
  [t, c, i, g[c][i % g[c].size]]
}
f[-1]  # => [-1, 0, -1, :a]
f[0]   # => [0, 1, 0, :b]
f[1]   # => [1, 0, 0, :a]
f[2]   # => [2, 1, 0, :b]
f[3]   # => [3, 0, 0, :a]
f[4]   # => [4, 1, 1, :c]
f[5]   # => [5, 0, 1, :a]
f[6]   # => [6, 1, 1, :c]
f[7]   # => [7, 0, 1, :a]
f[8]   # => [8, 1, 2, :d]
f[9]   # => [9, 0, 2, :a]
f[10]  # => [10, 1, 2, :d]
f[11]  # => [11, 0, 2, :a]
# t = 11 と -1 は同じ角度
  • メリット
    • 奇数人数でも先後が入れ替わらない (重要)
    • 誰がどのチームであるかが明白
    • 計算も若干シンプルになる
  • デメリット
    • データ構造がやや複雑
      • それに合わせて UI も(作るのが)難しい
    • 自分 vs 自分はやりにくい
      • 相手チームの手番で絶対に自分は選択されないため
  • 向いているケース
    • 人数を気にせずみんなで遊びたい場合
  • 対応するUI
    • チームを分ける意味で配列が二ついる
      • 一つの配列でもできなくはないがチーム分けボタンと順序変更ボタンがまざってひどいUIになってしまう
    • ゲームマスターのスムーズな仕切りを考慮すると、それぞれのチームに順序を考慮しつつメンバーをDnDできる直感的なUIが必要になる

実装時に気をつける

  • ロジックをベタ書きしてはいけない
  • Strategy パターンで抜き出す
  • Strategy には具体的なメンバー配列を渡してはいけない
    • Strategy が知りたいのはメンバー配列ではなくそのサイズ
    • 疎結合を意識する
  • Strategy 化のメリット
    • 絶対にバグってはいけない部分にフォーカスしてテストを書ける
    • アルゴリズムを切り替えるのが簡単になる
      • 例えば上の「データ構造を生かす案」に切り替える
      • または「手番がランダムに決まるルール」に切り替える等

観戦者とUI

観戦の有無の機能を追加しようとなったとき、データ構造1の一つの配列では各要素に「参加/不参加(観戦)」を切り替えるボタンを入れるのはいまいちで、上下移動ボタンがすでにあった場合、ごちゃごちゃして使いづらいUIになってしまう。
一方、データ構造2の二つの配列では、観戦者チームを意味する三つ目の配列を用意するだけで済む。
したがって観戦者チームの配列を用意することになるのであれば最初からデータ構造2で進める方が適している。

まとめ

  • 2人以上の偶数で均等に分かれて戦うのが明白な場合はどちらのデータ構造でもよい
  • ただし次の場合はチーム別に分けるデータ構造にする
    • 4人目がこなくてなかなかゲームが始められない
    • 4人で遊んでいるところに5人目が来て気まずい思いをさせてしまっている
    • 1対3で遊びたい

Discussion

ログインするとコメントできます