【多次元対応!?】TUIで動くマインスイーパ作ってみた

2022年11月01日 火曜日


【この記事を書いた人】
安井 裕亮

IIJで主に法人向けブロードバンド接続サービスのシステム開発・運用を担当しています。趣味はコーディング、海外旅行、音楽鑑賞、そして宇宙に想いを馳せること。Python / VS Code / Emacs / HHKB / Network

「【多次元対応!?】TUIで動くマインスイーパ作ってみた」のイメージ

初めまして。IIJ で主に法人向けブロードバンド接続サービスのシステム開発・運用を担当しております 安井(ysk)と申します。この度、社内SNSに投稿した記事が少しbuzzりまして、IIJ Engineers Blog の運営の方よりお声がけいただき寄稿の機会をいただきました。当ブログの他の記事に比べるとあまり技術っぽさ、IIJ っぽさが少なめかと思いますが、IIJ にはこんな変わったエンジニアも居るんだなぁ程度に見ていただき流していただけると幸いです。

このあと長めの自己紹介が続くので 本題が読みたい方はこちら からご覧ください。

自己紹介

初投稿ですので少し自己紹介をさせていただきます。

安井 裕亮(やすい ゆうすけ)と申します。生まれは愛知県で田んぼや緑が多い田舎でのびのびと育ちました。大学も愛知県内で IIJ に入社するタイミングで上京しました。最近はめっきり活動していないのですが、幼少期から楽器をやっており、中学では合唱のピアノ伴奏を、高校、大学時代には Jazz ビッグバンドに所属しドラムを演奏していました。

IIJ では主に法人向けのブロードバンド接続サービスに関わるシステムの開発・運用に従事しています。現在は主にブロードバンドを対象に扱っていますが、ADSLやISDN、はたまたダイヤルアップ接続の時代に遡るとインターネット接続系のサービスは長い歴史がありますので、システムも歴史あるものが多いです。そんなレガシーなシステムたちと日々奮闘したり、新たなシステムを開発してリプレースしたりしています。

IIJ ではエンジニアに特に固有の肩書がありませんが、敢えて自分で1つ肩書を付けるとしたらソフトウェアエンジニアだと思っています。ネットワークエンジニアとしてのキャリアも新人1年目の時に4ヶ月ほど積みましたが、ネットワークに関しては殆ど素人です。

以前、ITmediaさんの方で IIJ で働くエンジニアのデスクを紹介する記事がありましたが、私もこれに便乗する形で自身のオフィスのデスクをご紹介したいと思います。

とはいえ、ITmediaさんの記事でご紹介された方々のデスクと比較すると、あまり面白い点はありません(笑)

ディスプレイは LG の 27UL550-W(4K)と EIZO の FlexScan EV2736W(WQHD)です。マシンはメイン機が Mac mini (2018) で Laptop が Surface Go 2 です。左下端に少し見えていますが、NETGEAR の GS108PE と IIJ が開発しているルータである SEIL(SEIL/X1)を置いてミニラックを構築しています。キーボードは私物で HHKB Professional HYBRID(墨)の無刻印モデルを愛用しています。

無刻印モデルは文字のプリントがなくスッキリしてかっこいいのですが、未だに数字を打つ際は苦労しています。(不思議とコーディング時やIPアドレスを打つ際には困らないです。)

学生時代は HHKB Professional2 Type-S(白)を使用していました。こちらには HHKB の生みの親であり我らが IIJ が誇る和田英一先生にサインをいただきました。現在は家宝として自宅に保管しております。

エンジニアっぽいところをお見せするために、自宅の設備も少しご紹介したいと思います。

(配線が見苦しくて申し訳ありません…)

構成は至ってシンプルで、どこのご家庭でもよくある一般的な構成だと思います(?)全ての機器が1つのL2スイッチに収容されています。使用機材は概ね以下の通りです。

  • GE-ONU (NTT)
  • RTX1200 (YAMAHA)
  • UNIVERGE IX2015 (NEC)
  • JGS524E (NETGEAR)
  • GS108PE (NETGEAR)
  • Aterm WG600HP (NEC)
  • Aterm WG1800HP4 (NEC)
  • PRIMERGY TX1310 M1 (Fujitsu)
  • 自作PC x2

物理構成だけでは面白みに欠けるので、論理構成図もお見せします。

(画像をクリックすると拡大するので、拡大して御覧ください。)

回線はNTT東日本が提供する フレッツ 光ネクスト を利用しています。ひかり電話を契約しています。ISP は IIJ が提供する mio FiberAccess/NF を利用しています。IPoE によるIPv6接続と DS-Lite によるIPv4接続が提供されます。IPv6及びIPv4の接続形態は IIJ が別途提供している IIJmioひかり 相当だと認識いただいて差し支えないと思います。

主要なルータは RTX1200 でNGN網内から DHCPv6-PD により /56 のIPv6アドレスプレフィックスを受けています。RTX1200 がNGN網との境界のルータとなっています。さらに、この RTX1200 が図中の v6-global-01 及び v6-global-88 に対してそれぞれ /56 から切り出した /64 のIPv6アドレスプレフィックスを RA で広告しています。また生活用のネットワークである private に対して DS-Lite によるIPv4接続の提供と、DHCPによるIPv4プライベートアドレスの割り当てを行っています。自宅のPCや自分専用の Wi-Fi Access Point (Aterm WG600HP) や各種スマートホームデバイスなどがこのネットワークに接続されています。v6-global-88 に対しては /64 の RA での広告に加え、NGN網内から DHCPv6 で受けた情報をそのまま DHCPv6 で再配布しています。これにより Aterm WG1800HP4 はひかり電話契約なしの状態のNGN網に接続されたと認識しtransixモードで動作します。IPoE によるIPv6接続と DS-Lite によるIPv4接続の両方を提供してくれます。Aterm WG1800HP4 はゲスト用の Wi-Fi Access Point となっており、リモートワークで自宅に持ち込んだ会社のPCや会社支給のスマートフォンなどもこの Access Point に接続しています。

infra-native はいわゆるネイティブVLANになっており、L2スイッチや物理サーバ、各種仮想サーバ、ルータのオペレーション用のインタフェースがこのネットワークに接続されています。infra-gw はインターネットへの接続性を提供するルータや外部のクラウドサービスとの間でVPNを張るルータなどが接続するネットワークです。OSPF により経路交換をしています。

phone はIP Phone周りのネットワークです。前述の通り、我が家ではNTT東日本が提供するひかり電話を契約しています。それを活用して自前の IP-PBX を導入し、内線網を構築し、外線の発着信を可能にしています。

インターネット接続についてですが、IIJ が提供する mio FiberAccess/NF 以外に AS59105 とBGPピアを張っており、/28 のIPv4アドレス及び、/56 のIPv6アドレスの提供を受けています。割り当てられたグローバルアドレスは主に外部からアクセスするためのVPNのエンドポイントとして活用したり、自宅サーバのアドレスなどに活用させていただく予定です。UNIVERGE IX2015 が AS59105 の設備と EtherIP のトンネルを張っており、これにより構築されたL2ネットワーク (peering-seg-with-AS59105) 上で BGP Peer Router (VyOS) が AS59105 と BGP により経路交換をしています。AS59105 からは IPv4、IPv6 共にフルルートを提供していただいております。

eden.ysk.land は主に友人に提供しているネットワークや設備です。gate of ysk.land を介して外部から VPN (L2TP/IPsec) でこのネットワークやサーバ群にアクセスできます。

TUIで動くマインスイーパ

前置きが長くなりましたがここからが本題です。TUIで動くマインスイーパのお話です。

記事のタイトルが “作ってみた” となっていますが、(β版程度のものではありますが)やっと正式にリリースしたというのが正しいと思います。構想や初期の実装は自分が学生の頃からあった気がします。2022/10/14 現在 v0.2.1 をリリースしています。ruby 2.7 以上が必要です。

どんなものか実際に見ていただくのが早いと思うので、まずは以下のスクリーンショットをご覧ください。

操作は “k”, “j”, “h”, “l” で上下左右のカーソル移動、スペースでセルを開く、”f” または “!” でフラグを立てるです。

TUIですので Window System がインストールされていないサーバなどでも動かすことが出来ます。しかしこのマインスイーパ、ただTUIで動くだけのマインスイーパではありません。特徴的な機能をご紹介します。

隣接セルのフラグを一度に立てる

カーソル位置の隣接地雷数(カーソル位置に書かれている数字)とまだ開かれていない隣接セルの数が一致している場合、一度にフラグを立てることが出来ます。(カーソル位置で “f” キーもしくは “!” キーを押す)

ちなみに隣接地雷数とフラグが立っている隣接セルの数が一致している位置でスペースキーを叩くと周辺のセルを一度に開くことが出来ます。(Windows版のマインスイーパに実装されていた左右両クリックのようなものですね。)

–auto モード

隣接セルのフラグを一度に立てる機能と周辺セルを開く機能を悪用(笑)したものです。片手が塞がっていて忙しい方でもらくらくクリアできます。しかし、もはやマインスイーパのゲーム性が疑われます。

そして次にご紹介する機能がこのTUIマインスイーパの最も目玉とも言える機能です。

多次元対応

このマインスイーパ、なんと最大で4次元まで対応しているのです!

(↑自分で作っておいて頭が追いつかず混乱しています…)
3次元、4次元の境界をまたぐ際は “K”, “J”, “H”, “L” を用います。

3次元はもちろん、1次元や、はたまた 0次元 まで対応しています。

↓1次元で動かしている例

0次元 で動かしている例

なお、0次元ではセルが1つしかありませんので、指定できる地雷数は0のみです。ゲームとしては全く面白くないですが、時間がなくてサクッとゲームを終わらせたい方にはオススメです😊

どうやって多次元に対応しているのか

これまた自作のgemなのですが、MdArray(= multi-dimensional array)というものを使って実現しています。

この MdArray はn次元配列の任意の要素の “隣接” 及び “近傍” の要素を列挙することが出来ます。コードをお見せしながら説明したいと思います。

MdArray のコード上では “隣接” を “adjacent”、“近傍” を “neighborhood” と呼んでいます。一般的にはそれぞれ ノイマン近傍、ムーア近傍 という呼び名があるようです。マインスイーパで重要になってくるのは “近傍”(= “neighborhood” = ムーア近傍)なのですが、まずはよりシンプルな “隣接”(= “adjacent” = ノイマン近傍)を求める方法から説明したいと思います。

3次元空間に立方体のセルが並んでいる様子を想像してみてください。あるセルの “隣接” はまずx, y座標が同一でz軸方向に -1, +1 した位置にあるセルが該当します。同一z座標上ではx軸、y軸方向にそれぞれ片方ずつ -1, +1 した位置にあるものが該当するのですが、これは見方を変えると同一z座標上の2次元平面上で隣接しているものが該当すると考えることが出来ます。これをn次元に一般化するとこうです。まず第1〜第n-1座標が同一で第n軸方向に -1, +1 した位置にあるセルが該当します。同一第n座標上では、そのn-1次元平面上で隣接しているものが該当します。これはコード上では再帰的定義を用いて表現することが出来ます。

以下のコードは実際の MdArray のコードを説明向けに少し改変したものです。MdArray では下位次元の多次元配列を MdArray オブジェクトのリストとして持つ再帰的な構造になっています。@sub_arrays が下位次元(n-1次元)の MdArray オブジェクトを保持する配列です。adjacent の引数である index は多次元配列のインデックスなのですが、多次元なのでn次元であればサイズnの配列になります。境界のチェックが入っているので少し見づらいですが、先ほど一般化として言語で表現したものがコードで表現されていることが分かると思います。

def adjacent(index)
  if @sub_arrays
    index_n, *sub_index = index
    size_n = @sub_arrays.length

    Enumerator::Chain.new(
      (0...size_n) === index_n - 1 ?
      [@sub_arrays[index_n - 1].at(sub_index)] : [],
      @sub_arrays[index_n].adjacent(sub_index),
      (0...size_n) === index_n + 1 ?
      [@sub_arrays[index_n + 1].at(sub_index)] : []
    )
  else
    []
  end
end

続いて本題の “近傍” になります。いきなり一般化してしまいますが、n次元におけるあるセルの “近傍” はこうです。まず第1〜第n-1座標が同一で第n軸方向に -1, +1 した位置にあるセルが該当します。さらにその2つのセルの同一第n座標上のn-1次元平面で近傍に位置するセルも該当します。またさらに、元のセルと同一第n座標上のn-1次元平面で近傍に位置するセルも該当します。これをコードに落としたものが以下となります。(Chainを多用していて見づらいコードになっており申し訳ありません…)

def neighborhood(index)
  if @sub_arrays
    index_n, *sub_index = index
    size_n = @sub_arrays.length

    Enumerator::Chain.new(
      (0...size_n) === index_n - 1 ?
      Enumerator::Chain.new([@sub_arrays[index_n - 1].at(sub_index)],
                            @sub_arrays[index_n - 1].neighborhood(sub_index)) : [],
      @sub_arrays[index_n].neighborhood(sub_index),
      (0...size_n) === index_n + 1 ?
      Enumerator::Chain.new([@sub_arrays[index_n + 1].at(sub_index)],
                            @sub_arrays[index_n + 1].neighborhood(sub_index)) : []
    )
  else
    []
  end
end

今回紹介したTUIマインスイーパはこの MdArray が提供する近傍の要素を割り出す機能を用いて実現されているのです。MdArray はn次元に対応しているので、実はこのマインスイーパも内部的にはn次元に対応しています。しかし適切なレンダリング方法を思いつかなかったため、4次元を上限としています。

以上、TUIで動くちょっと変わったマインスイーパのご紹介でした。Windows等に実装されていたマインスイーパは縦横のセル数、配置できる地雷数などに制約があったかと思いますが、このTUIマインスイーパにはそのような制約はありません。どんなサイズ、地雷数も思いのままです。

(↑巨大なセル数を指定し 総セル数 – 1 個の地雷を埋めて、優越感に浸っている例)

前述の通り Window System を必要としませんので、ログインシェルに指定したり、クリアできたら通常のshellが立ち上がるように仕込んだりと色々活用できそうです。ssh越しにマインスイーパを提供するサービスなども作れそうですね。ぜひ皆さん様々な環境で遊んでみてください。

また、5次元以上のレンダリング方法を思いついた方、もしくはカッコいい3次元、4次元のレンダリング方法を思いついた方はぜひ実装してみてください!

謝辞

今回このようなネタで寄稿の機会を与えてくださった IIJ Engineers Blog の運営の皆さまに感謝いたします。また、社内SNSでの投稿において bug fix にご協力いただいたり、ご指摘をいただいた IIJ のエンジニアの方々に感謝いたします。

安井 裕亮

2022年11月01日 火曜日

IIJで主に法人向けブロードバンド接続サービスのシステム開発・運用を担当しています。趣味はコーディング、海外旅行、音楽鑑賞、そして宇宙に想いを馳せること。Python / VS Code / Emacs / HHKB / Network