ASP.NETお試し会

本記事はMuroran Institute of Technology Advent Calendar 2019 12/7の記事です.

adventar.org

はじめに

唐突にASP.NETお試し会を開催しました. 参加者は3名(表示名:参加者1,参加者2,参加者3)です. 内容はASP.NETを用いたジャンケンAPIの試作です. 以下では,開催に至った経緯,ASP.NETお試し会でやったこと,試作したジャンケンAPIの動作例,3名の感想を書きます.

開催に至った経緯

参加者1は普段からUnityでゲーム開発をしています. さらに,参加者1はオンラインゲームの開発に挑戦しようとしています. 参加者1は,特にサーバサイドにおける通信を扱うプログラミングについて,参加者2から教えてもらうことにしました. 参加者2は,参加者1がC#に慣れていると考え,ASP.NETを利用してサーバプログラムを開発したら良いと考えました. しかし,参加者2はサーバプログラムの開発経験はKtorを利用した簡単なWebAPIの開発だけであり,ASP.NETについては全く知らないため教えることができません. 参加者2はKtorを用いないサーバプログラム開発にも興味を持っていたため,参加者1と一緒に調べながら簡単なサーバプログラムを試作し,理解を深めることにしました. そこに参加者3が合流して,ASP.NETお試し会が開催されることとなりました.

ASP.NETお試し会でやったこと

ASP.NETお試し会では以下の作業を行いました.

  1. VisualStudioの起動とプロジェクトの作成
  2. ジャンケンAPIの試作
    1. モデルの構成
    2. コントローラーの構成

VisualStudioの起動とプロジェクトの作成

VisualStudioの起動して,「Welcome Page」から「New Project...」,「ASP.NET Core Web API」,「Next」と進み,「Project Name」と「Solution Name」を埋め,「Create」を選択してプロジェクトを作成しました. 実行し,ブラウザからhttp://localhost: 24635/api/valuesにアクセスすると["value1","value2"]と表示されました. ポート番号は人にプロジェクトによって異なるようです. 初期状態ではこのURLにアクセスするとControllers/ValuesController.cspublic IEnumerable<string> Get()メソッドが呼ばれているようです. Controllers/ValuesController.csを以下に示します.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace Test3.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

ジャンケンAPIの試作

ASP.NETのプロジェクトの初期動作を確認した後,以下のモデルとコントローラを実装し,ジャンケンAPIを試作しました.

モデルの構成

モデルはゲームのリストとゲームを操作するコマンドで構成されます. ゲームはプレイヤ2人の名前と出した手(グー,チョキまたはパー)を保持します. ゲームを操作するコマンドは以下の通りです.

  • make:
    • 入力:プレーヤー0の名前
    • 出力:ルームID
    • 新たなゲームを作成する
  • enter :
    • 入力:入室する部屋のルームID,プレーヤー1の名前
    • 出力:なし
    • 既存の部屋に入室する
  • tryPlay :
    • 入力:ルームID
    • 出力:プレイを開始できるかどうか
    • プレイできるか確認する
  • play :
    • 入力:ルームID,プレーヤー名,グー,チョキまたはパー
    • 出力:なし
    • グー,チョキまたはパーを出す
  • tryResult:
    • 入力:ルームID
    • 出力:勝敗を判定できるかどうか
    • 勝敗を判定できるか確認する
  • result :
    • 入力:ルームID
    • 出力:勝者と敗者
    • 勝敗を確認する

以上のモデルを実装したModelクラスを以下に示します.

using System;
using System.Collections.Generic;

namespace Aspdotnet.Models
{
    public enum Move
    {
        Rock,
        Paper,
        Scissors,
        None
    }

    public class Result
    {
        String Winner;
        String Loser;
        public Result(String winner, String loser)
        {
            Winner = winner;
            Loser = loser;
        }
        public bool IsTie() { return Winner == null && Loser == null; }
        public override string ToString()
        {
            return "Winner: " + Winner + ", " + "Loser: " + Loser;
        }
    }

    class Game
    {
        public String player0;
        public Move move0 = Move.None;
        public String player1;
        public Move move1 = Move.None;

        public bool IsReady()
        {
            return player0 != null && player1 != null && move0 == Move.None && move1 == Move.None;
        }

        public bool HasFinished()
        {
            return player0 != null && player1 != null && move0 != Move.None && move1 != Move.None;
        }

        public Result GetResult()
        {
            if (!HasFinished())
            {
                return null;
            }
            if (move1 == Move.Paper && move0 == Move.Scissors ||
                move1 == Move.Scissors && move0 == Move.Rock ||
                move1 == Move.Rock && move0 == Move.Paper)
            {
                return new Result(player0, player1);
            }
            if (move0 == Move.Paper && move1 == Move.Scissors ||
                move0 == Move.Scissors && move1 == Move.Rock ||
                move0 == Move.Rock && move1 == Move.Paper)
            {
                return new Result(player1, player0);
            }
            return new Result(null, null);
        }
    }

    public class Model
    {
        List<Game> games = new List<Game>();

        static Model instance = new Model();

        public static Model GetInstance() { return instance; }

        Model() { }

        public override string ToString()
        {
            string s = "";
            for (int i = 0; i < games.Count; ++i)
            {
                var g = games[i];
                s += i.ToString() + " : ";
                s += "player0 = " + g.player0 + "; ";
                s += "player1 = " + g.player1 + "; ";
                s += "move0 = " + g.move0 + "; ";
                s += "move1 = " + g.move1 + "; ";
            }
            return s;
        }
        public int Make(String player0)
        {
            if (player0 == null)
            {
                return -1;
            }
            int roomId = games.Count;
            var game = new Game();
            game.player0 = player0;
            games.Add(game);
            return roomId;
        }

        public void Enter(int roomId, String player1)
        {
            if (player1 == null)
            {
                return;
            }
            if (roomId < 0 || roomId >= games.Count)
            {
                return;
            }
            var game = games[roomId];
            game.player1 = player1;
        }

        public bool TryPlay(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return false;
            }
            var game = games[roomId];
            return game.IsReady();
        }

        public void Play(int roomId, String player, Move move)
        {
            if (roomId < 0 || roomId >= games.Count || move == Move.None)
            {
                return;
            }
            var game = games[roomId];
            if (game.player0 == player && game.move0 == Move.None)
            {
                game.move0 = move;
            }
            if (game.player1 == player && game.move1 == Move.None)
            {
                game.move1 = move;
            }
        }

        public bool TryResult(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return false;
            }
            return games[roomId].HasFinished();
        }

        public Result Result(int roomId)
        {
            if (roomId < 0 || roomId >= games.Count)
            {
                return null;
            }
            var game = games[roomId];
            if (!game.HasFinished())
            {
                return null;
            }
            return games[roomId].GetResult();
        }
    }
}
コントローラーの構成

クライアントがHTTPのGETメソッドを用いてジャンケンAPIにアクセスして,上のコマンドを実行できるようにします. コマンドへの入力はURLに含めることにしました. これを実装したJankenControllerクラスを以下に示します.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace Aspdotnet.Controllers
{
    [Route("api/[controller]")]
    public class JankenController : Controller
    {
        // GET api/janken/make/player0Name
        [HttpGet("make/{player0}")]
        public int GetMake(string player0)
        {
            var model = Models.Model.GetInstance();
            int roomId = model.Make(player0);
            Console.WriteLine(model);
            return roomId;
        }

        // GET api/janken/enter/roomId/player1Name
        [HttpGet("enter/{roomId}/{player1}")]
        public string GetEnter(int roomId, string player1)
        {
            var model = Models.Model.GetInstance();
            model.Enter(roomId, player1);
            Console.WriteLine(model);
            return "";
        }

        // GET api/janken/tryPlay/roomId/
        [HttpGet("tryPlay/{roomId}")]
        public bool GetTryPlay(int roomId)
        {
            var model = Models.Model.GetInstance();
            var isReady = model.TryPlay(roomId);
            Console.WriteLine(model);
            return isReady;
        }

        // GET api/janken/play/roomId/player0Name/Rock
        [HttpGet("play/{roomId}/{player}/{move}")]
        public string GetPlay(int roomId, string player, string move)
        {
            var model = Models.Model.GetInstance();
            Models.Move m = Models.Move.None;
            switch (move)
            {
                case "Rock":
                    m = Models.Move.Rock;
                    break;
                case "Paper":
                    m = Models.Move.Paper;
                    break;
                case "Scissors":
                    m = Models.Move.Scissors;
                    break;
                default:
                    return "";
            }
            model.Play(roomId, player, m);
            Console.WriteLine(model);
            return "";
        }

        // GET api/janken/tryResult/roomId/
        [HttpGet("tryResult/{roomId}")]
        public bool GetTryResult(int roomId)
        {
            var model = Models.Model.GetInstance();
            bool hasFinished = model.TryResult(roomId);
            Console.WriteLine(model);
            return hasFinished;
        }

        // GET api/janken/result/roomId/
        [HttpGet("result/{roomId}")]
        public string GetResult(int roomId)
        {
            var model = Models.Model.GetInstance();
            Models.Result result = model.Result(roomId);
            Console.WriteLine(model);
            return result.ToString();
        }
    }
}

試作したジャンケンAPIの動作例

最後に試作したジャンケンAPIの動作確認をしました. 動作例を以下に示します.

curl http://localhost:24635/api/janken/make/Sankasha1
# => 1
curl http://localhost:24635/api/janken/enter/0/Sankasha2
curl http://localhost:24635/api/janken/tryPlay/0
# => true
curl http://localhost:24635/api/janken/play/0/Sankasha1/Paper
curl http://localhost:24635/api/janken/play/0/Sankasha2/Rock
curl http://localhost:24635/api/janken/tryResult/0
# => true
curl http://localhost:24635/api/janken/result/0
# => Winner: Sankasha1, Loser: Sankasha2

3名の感想

参加者1

ASP.NETを通じて、HTTP通信を組み込んだソフトウェアをどう組み立てるか、どう実装した方が良いかを考える経験が出来ました

参加者2

見様見真似でとりあえず,動作するジャンケンAPIを作成することができた. その点では,やはりVisualStudioとASP.NETはすごいと思った. ただ,実際にVPSなどで動作させる場合にどうすれば良いか想像できなかった. 当たり前だが,今後ASP.NETを利用する場合には使い方をきちんと調べる必要がある.

ジャンケンAPIは,最初は単純なように思えたが,実際に試作してみると状態を管理するのは意外と面倒だった. また,入力が全てURLに含まれているため,不正なプレイが可能かもしれない.

ASP.NETお試し会を開催した結果,アドベントカレンダーを書くこともできため良かった.

参加者3

もう少し難しいと思ってたのですが,実装自体は予想以上に楽だったので驚きました.使い方次第でいろいろ面白いことが出来そうなので,自作ゲームで生かしてみたいです.