怒りに燃える無職が倉庫番アプリに再挑戦してみた C#

もうじき無職の進藤京介です。
以前、倉庫番アプリを作って公開しました
→<a href="https://qiita.com/shindokyosuke/items/0308fdb67bd239973a95"　 target="_blank">無職が倉庫番をコンソールアプリで作ってみた C#</a>

確かにアプリケーションとしては動きましたが、コメントを頂き自身の力不足を痛感致しました。
そして私はただひたすらに強くなる事だけを願い特訓をしてきました。
それでも何か超えられない壁があり、自分の限界を知った時、自分に対する怒りでスーパーサイヤ人へと進化致しました。

御託はもういいですね。というわけで再挑戦しました。ちょっと長くなります。ごめんなさい。


アプリケーションでの前回からの進化点は、ゴールの上をプレイヤーが乗る仕組みを実現したくらいで、あとはほとんど変わっておりません。
ルールも大体同じです。

とても長くなってしまうので完璧:thumbsup_tone3:なUML図を用意させていただきました。

![kyosukeshindou_gm.png](https://qiita-image-store.s3.amazonaws.com/0/201764/743109a8-0730-280c-9ba4-cec4c05f86ff.png)
<font color="Red">※ガチ初心者です。本当にごめんなさい。小学生の落書きだと思ってください:cry:下に変な矢印まであります:bow:</font>


##CharManager

今回もプレイヤーなどのオブジェクトを表現するのにcharを使っています。
これらの文字を扱う簡単なクラスを使いました

```csharp:CharManager.cs
namespace soukoban_ex
{
    public class CharManager
    {
        public static char getPlayerChar(){
            return 'P';
        }

		public static char getPlayerOnGoalChar()
		{
			return 'O';
		}

		public static char getNimotuChar()
		{
			return 'N';
		}

		public static char getNimotuOnGoalChar()
		{
			return 'F';
		}

		public static char getFloorChar()
		{
			return '□';
		}

		public static char getGoalChar()
		{
			return 'G';
		}
    }
}

```
やっていることは必要な文字を返却するだけです。
壁（■)に関しては特にすることがないので省いています。もし必要になったら、足りなくなったら足すだけやから。という感じです。

##MainClass

ではMainClassです。

```csharp:MainClass.cs
using System;

namespace soukoban_ex
{
    class MainClass
    {
        public static void Main(string[] args)
        {

            Console.WriteLine("遊び方。\n a=左移動:d=右移動:w=上移動:s=下移動");
            Console.WriteLine("P=プレイヤー");
            Console.WriteLine("N=荷物");
            Console.WriteLine("G=ゴール");
            Console.WriteLine("■=壁");
            Console.WriteLine("F=ゴールに乗った荷物");
            Console.WriteLine("O=ゴールに乗ったプレイヤー");

            Stage stage = new NomalStage();
            var filePath = @"/Users/kyosuke/Projects/sokoban/sokoban/StageData.txt";
            stage.startGame(filePath);
        }
    }
}

```

Stageを作ってゲームをスタートさせています。


##Stage

```csharp:Stage.cs
using System;
using System.IO;

namespace soukoban_ex
{
    public  abstract  class Stage
    {
        protected int stageWidth;
        protected string[] allStageChar;
        protected ObjectManager objectManager = new ObjectManager();

        public void startGame(String filepath){
            readMap(filepath);
            createObject();
            update();
        }

        private void readMap(String filePath){
            allStageChar = File.ReadAllLines(filePath);
			stageWidth = allStageChar[0].Length;
        }

        protected void CheckGoal(){
            
            if (!objectManager.hasGoal())
			{
				Console.WriteLine("ゲームクリア");
				Environment.Exit(0);
			}
        }

        protected abstract void createObject();
        protected abstract void update();

    }
}
```

Stageクラスはゲームのエンジンのようなものです。
マップを読み込みオブジェクトを生み出しループ処理したり。というのを他のクラスに投げたりしています。


##InputManager

サブクラスの前に入力を受け取るクラス(InputManager)です


```csharp:InputManager.cs

using System;
namespace soukoban_ex
{
    public class InputManager
    {
        public InputManager(){}
      
        public int getInputKey(int stageWidth){
            
			string key = Console.ReadLine();
			int direction = 0;
			//入力したキーに応じてプレイヤーの移動先を決定します。
			switch (key)
			{
				case "a":
					direction = -1;
					break;
				case "d":
					direction = 1;
					break;
				case "w":
					direction = stageWidth * (-1);
					break;
				case "s":
					direction = stageWidth;
					break;
				default:
					Console.Write("ゲームを終了します");
					Environment.Exit(0);
                    return 0;
			}
            return direction;
        }
    }
}

```

InputManagerは
入力を受け取って移動先となるdirectionを決定し返却するgetInputKeyメソッドを持っています。

##NomalStage

ではStageのサブクラスです


```csharp:NomalStage.cs

namespace soukoban_ex
{
    public class NomalStage : Stage
    {
       
        protected override void createObject(){
            Factory factory = new GameObjectFactory();
            foreach(var lines in allStageChar){
                foreach(var ch in lines)
                    objectManager.addObject(factory.create(ch));
             }
        }

        protected override void update(){
            
            InputManager inputManager = new InputManager();
            ActionManager actionManager = new ActionManager(objectManager);
            objectManager.printObject(stageWidth);

            while(true){

                var player = objectManager.getPlayerObject();
                var direction = inputManager.getInputKey(stageWidth);
                var nextObj = objectManager.getNextObject(direction);


                if (nextObj.objectType == GameObject.ObjectType.WALL){
					objectManager.printObject(stageWidth);
					continue;
                }

                if(nextObj.objectType == GameObject.ObjectType.NIMOTUONGOAL){
                    objectManager.printObject(stageWidth);
                    continue;
                }

                if(nextObj.objectType == GameObject.ObjectType.GOAL){
                    actionManager.hitGoal(direction);
                    objectManager.printObject(stageWidth);
                    continue;
                }

                if(nextObj.objectType == GameObject.ObjectType.NIMOTU){
                    actionManager.hitNimotu(direction);
                    objectManager.printObject(stageWidth);
					
                    CheckGoal();
                    continue;
                }

                if(player.type == CharManager.getPlayerChar())
                    player.changeType(CharManager.getFloorChar(), GameObject.ObjectType.FLOOR);
                else
                    player.changeType(CharManager.getGoalChar(), GameObject.ObjectType.GOAL);
                
                nextObj.changeType(CharManager.getPlayerChar(), GameObject.ObjectType.PLAYER);
                objectManager.printObject(stageWidth);

            }
        }

    }
}

```

必要なものをオーバーライドします
createObjectではオブジェクト生成に必要なFactoryをインスタンス化し、
objectManagerというオブジェクト管理しているクラスにオブジェクトをぶち込んでいます。

updateは、InputManagerから受け取った値でプレイヤーの移動先を調べています。
移動先のオブジェクトに応じて、ActionManagerクラスのメソッドを呼び出しています。

移動先が特に何もない床(□)ならば、プレイヤーと入れ替えるようにして終わりですが、プレイヤーがゴールに乗っている場合は、単純な入れ替えではゴールが床になってしまうので、そこを無理やり誤魔化しています。

```csharp:NomalStage.cs
if(player.type == CharManager.getPlayerChar())
       player.changeType(CharManager.getFloorChar(), GameObject.ObjectType.FLOOR);
else
       player.changeType(CharManager.getGoalChar(), GameObject.ObjectType.GOAL);

```

オブジェクトの持つchangeTypeというメソッドを使って書き換えを行なっています。


##GameObject

そしてそのプレイヤーなどを表現するクラスです。
GameObjectクラスという名前にしました。これを継承したクラスを作るのですが、今回までならば、このクラスを抽象クラスにせずに済んだのですが、やっちゃいました。
これはクリリンが18号を破壊するスイッチを押せなかったようなもんですね。。。。

```csharp:GameObject.cs
namespace soukoban_ex
{
    public abstract class GameObject
    {

        public enum ObjectType{
            PLAYER,
            NIMOTU,
            GOAL,
            WALL,
            FLOOR,
            NIMOTUONGOAL,
        }

        public int position{
            get;protected set;
        }

        public char type{
            get;protected set;
        }

        public ObjectType objectType{
            get;protected set;
        }

        public void init(int pos, char typech,ObjectType objtype)
		{
			position = pos;
			type = typech;
            objectType = objtype;
		}

        public virtual void changeType(char type , ObjectType objecttype){}

    }
}
```
持っているものは単純で、オブジェクトのタイプと位置情報と文字です。
ゴールの上に乗っているプレイヤーは描画する時の識別で、特にすることがないのでタイプの中には入れてあげませんでした。

##SoukobanObject

```csharp:SoukobanObject.cs
namespace soukoban_ex
{
    public class SoukobanObject : GameObject
    {
        public override void changeType(char type, GameObject.ObjectType objecttype)
        {
            this.type = type;
            this.objectType = objecttype;
        }
	}
}

```

サブクラスはchangeTypeを実装しただけです。はい。
文字を切り替えて、タイプを変更するという処理です。


##Factory
先ほど登場しスルーされたFactroyを通してこのゲームオブジェクト達を生成しています。

```csharp:Factory.cs
using System;
namespace soukoban_ex
{
    public abstract class Factory
    {
        public GameObject create(char type){
            GameObject gameobject = createGameObject(type);
            return gameobject;
        }
        protected abstract GameObject createGameObject(char type);
    }
}
```

これを継承したクラスはcreateGameObjectをオーバーライドし、そこで具体的なインスタンスの生成方法を決定します。

##GameObjectFactory

```csharp:GameObjectFactory.cs
namespace soukoban_ex
{
    public class GameObjectFactory : Factory
    {
        public GameObjectFactory(){}
        private int positionCounter = 0;

        protected override GameObject createGameObject(char type){

            positionCounter++;
            GameObject gameobject = null;
			
            switch(type){
                case 'P':
                    gameobject = new SoukobanObject();
                    gameobject.init(positionCounter,type,GameObject.ObjectType.PLAYER);
                    break;
                case '■':
                    gameobject = new SoukobanObject();
                    gameobject.init(positionCounter, type,GameObject.ObjectType.WALL);
                    break;
				case 'N':
                    gameobject = new SoukobanObject();
                    gameobject.init(positionCounter, type,GameObject.ObjectType.NIMOTU);
					break;
				case 'G':
					gameobject = new SoukobanObject();
                    gameobject.init(positionCounter, type, GameObject.ObjectType.GOAL);
					break;
                 default:
                    gameobject = new SoukobanObject();
                    gameobject.init(positionCounter,type,GameObject.ObjectType.FLOOR);
                    break;
            }
            return gameobject;
        }

    }
}
```

やっていることは、受け取った文字列に応じてゲームオブジェクトのタイプを決定し、カウンターの値で位置情報を設定しています。

前回は配列にオブジェクト突っ込んで、配列の順番で描画処理をしていましたが、今回はこのオブジェクトの持つ位置情報から描画処理を行います。


##ObjectManager

ゲームオブジェクトをStageで管理させることを考えた時、Stageクラスの処理が膨大に膨れ上がっていったのです。私はこう感じました。これでは前回と同じじゃないか！と。

というわけでゲームオブジェクトを管理するクラスを作りました。

```csharp:ObjectManager.cs
using System;
using System.Collections.Generic;
using System.Linq;


namespace soukoban_ex
{
    public class ObjectManager
    {
       private List<GameObject> objectList = new List<GameObject>();

        public void addObject(GameObject gameobject){
            this.objectList.Add(gameobject);
        }

        public GameObject getPlayerObject(){
            var player = this.objectList.Find(s => s.objectType == GameObject.ObjectType.PLAYER);
            return player;
        }

        public GameObject getGameObjectWithPosition(int position){
            var gameObject = this.objectList.Find(s => s.position == position);
            return gameObject;
        }

        public GameObject getNextObject(int direction){
            var playerpos = getPlayerPosition();
            return getGameObjectWithPosition(playerpos + direction);
        }

        public GameObject getMoreNectObject(int direction){
			var playerpos = getPlayerPosition();
            return getGameObjectWithPosition(playerpos + direction + direction);
        }

        public int getPlayerPosition(){
            var player = getPlayerObject();
            return player.position;
        }

        public void printObject(int width){
			var printlist = objectList.OrderBy(x => x.position);
			int counter = 0;
			foreach (var obj in printlist)
			{
				counter++;
				Console.Write(obj.type);
				if ((counter) % width == 0)
					Console.Write(Environment.NewLine);
			}
		}

        public bool hasGoal(){
            var finish = objectList.Select(s => s).Any(s => s.objectType == GameObject.ObjectType.GOAL);
            return finish;
        }

    }
}
```

生成したオブジェクト達はaddObjectメソッドを通じて、Listの中に入っていきます。
あとはメソッド名通りのことをやっています。

printObjectは描画処理になります。
ゲームオブジェクトの位置情報で描画処理をするので、ポジションを並び替えてコンソールに出力をしております。

悔しかったのがhasGoalメソッドです。
これは、Listの中にGOALタイプがあればtureを返しています。
GOALタイプのオブジェクトが存在しているということは、まだ荷物の乗っていないゴールが存在しているということです。
果たしてこのメソッドはここにいても良いのかどうなのか、私は今も疑問に思っています。


StageはObjectManagerからプレイヤーやその移動先のオブジェクトを取得して、処理を行なっているという流れにしました。


##ActionManager

となると残すところは最後のアクション関係です。
このアプリケーションの頭脳担当のActionManagerクラスです。

このクラスはプレイヤーが荷物に衝突した時、ゴールに衝突した時に呼ばれるメソッドを持っています。

```csharp:ActionManager.cs
namespace soukoban_ex
{
    public class ActionManager
    {
        private ObjectManager objectManager = null;

        public ActionManager(ObjectManager objectManager){
            this.objectManager = objectManager;
        }

        public void hitGoal(int direction){
            
            var player = objectManager.getPlayerObject();
            var goal = objectManager.getNextObject(direction);

            player.changeType(CharManager.getFloorChar(),GameObject.ObjectType.FLOOR);
            goal.changeType(CharManager.getPlayerOnGoalChar(), GameObject.ObjectType.PLAYER);

        }

        public void hitNimotu(int direction){
            /*  まずは必要なオブジェクトを取得します　*/
            var player = objectManager.getPlayerObject();
            var nextObj = objectManager.getNextObject(direction);
            var morenextObject = objectManager.getMoreNectObject(direction);

            //荷物の移動先が荷物だったら動かせないので処理はしません
            if (morenextObject.objectType == GameObject.ObjectType.NIMOTU)
                return;
            //荷物の移動先が荷物の乗ったゴールだったら動かせないので処理はしません
            if (morenextObject.objectType == GameObject.ObjectType.NIMOTUONGOAL)
				return;
            /*
             * 荷物の移動先がゴールだったら
             *・ゴールに荷物を乗せて
             *・もともと荷物の場所にプレイヤーが移動して
             *・プレイヤーのいた場所を床にします。
             */
			if (morenextObject.objectType == GameObject.ObjectType.GOAL)
			{
                morenextObject.changeType(CharManager.getNimotuOnGoalChar(), GameObject.ObjectType.NIMOTUONGOAL);
                nextObj.changeType(CharManager.getPlayerChar(), GameObject.ObjectType.PLAYER);
                player.changeType(CharManager.getFloorChar(), GameObject.ObjectType.FLOOR);
				return;
			}
            //最終的に荷物の移動先が壁でなければ
            if (morenextObject.objectType != GameObject.ObjectType.WALL)
			{
                /*
                 *・荷物の移動先に荷物を移動して 
                 *・荷物の場所にプレイヤーが移動して
                 *・プレイヤーの場所が床になります
                 */
                morenextObject.changeType(CharManager.getNimotuChar(), GameObject.ObjectType.NIMOTU);
                nextObj.changeType(CharManager.getPlayerChar(),GameObject.ObjectType.PLAYER);
                player.changeType(CharManager.getFloorChar(),GameObject.ObjectType.FLOOR);
			}
        }

    }
}

```

処理が複雑だったのでコメントを入れてみました。
コンストラクタでObjectManagerを受け取ります。

```csharp:ActionManager.cs
        public void hitGoal(int direction){

            var player = objectManager.getPlayerObject();
            var goal = objectManager.getNextObject(direction);

            player.changeType(CharManager.getFloorChar(),GameObject.ObjectType.FLOOR);
            goal.changeType(CharManager.getPlayerOnGoalChar(), GameObject.ObjectType.PLAYER);

        }
```

hitGoalメソッドはプレイヤーをゴールの上に乗せます。
しかしゲームオブジェクトのタイプに、ゴールの上に乗ったプレイヤーというのは無いので、文字だけを切り替えています。

ゴールの上のプレイヤーに荷物が衝突することは絶対にありえないので、文字を切り替えるだけにしました。



以上になります。

色々と考えて紙に書いてこんな風に実装しようとか思っていたのですが、いざコードを打ってみるとうまく実現できなかったり、こんな簡単なものなのに結構苦労しました。
それでもプログラミングは楽しいということが実感できました。

この先、スーパーサイヤ人２を目指し、ブルーを目指し、身勝手の極意を身につけるためには、まだまだ相当なトレーニングが必要だと思いました。


非常に長くなってしまい申し訳ございませんでした。
最後までお付き合いいただきありがとうございました。