/// <summary>
        /// Create a task batch from a turn
        /// </summary>
        /// <param name="turn">the turn</param>
        /// <param name="sceneConfiguration">
        /// configuration of this scene
        /// </param>
        /// <param name="boardPositionLookUp">lookup service</param>
        /// <returns></returns>
        internal static TasksBatch ToTasksBatch(
            this VisualizerTurn turn,
            SceneConfiguration sceneConfiguration,
            BoardPositionLookUp boardPositionLookUp)
        {
            GameChange gameChange = turn.Change;
            GameState  gameState  = turn.State;

            TasksBatch batch = new TasksBatch();
            var        ignoreForHubUpdate = new HashSet <string>();

            ProcessCharacterChanges(
                sceneConfiguration,
                boardPositionLookUp,
                gameChange,
                gameState,
                batch,
                ignoreForHubUpdate);

            ProcessTileChanges(sceneConfiguration, gameState, gameChange, batch);

            ProcessGameState(batch, sceneConfiguration, gameState, ignoreForHubUpdate);

            return(batch);
        }
        private static Vector3[] GetPath(RepeatedField <Position> path, BoardPositionLookUp boardPositionLookUp)
        {
            Vector3[] newPath = new Vector3[path.Count];

            for (int i = 0; i < path.Count; i++)
            {
                Position position = path[i];
                newPath[i] = boardPositionLookUp.Translate(
                    new Vector3Int(position.X, position.Y, 0));
            }

            return(newPath);
        }
        private static void ProcessCharacterChanges(
            SceneConfiguration sceneConfiguration,
            BoardPositionLookUp boardPositionLookUp,
            GameChange gameChange,
            GameState gameState,
            TasksBatch batch,
            HashSet <string> ignoreForHubUpdate)
        {
            if (gameChange == null || gameState == null)
            {
                return;
            }

            foreach (var pair in gameChange.CharacterChanges)
            {
                string          entity          = pair.Key;
                Character       character       = null;
                CharacterChange characterChange = pair.Value;
                bool            isMonster       = false;

                if (gameState.PlayerNames.ContainsKey(entity))
                {
                    // if the entity is a player
                    character = gameState.PlayerNames[entity].Character;
                }
                else if (gameState.MonsterNames.ContainsKey(entity))
                {
                    // if the entity is a monster
                    character = gameState.MonsterNames[entity].Character;
                    isMonster = true;
                }
                else
                {
                    throw new Exception($"Don't know how to handle entity {entity}");
                }

                ProcessCharacter(
                    batch, entity,
                    character,
                    characterChange,
                    gameState,
                    sceneConfiguration,
                    boardPositionLookUp,
                    ignoreForHubUpdate,
                    isMonster);
            }
        }
        private static void ProcessCharacter(
            TasksBatch batch,
            string entity,
            Character character,
            CharacterChange characterChange,
            GameState gameState,
            SceneConfiguration sceneConfiguration,
            BoardPositionLookUp boardPositionLookUp,
            HashSet <string> ignoreForHubUpdate,
            bool isMonster)
        {
            var position = new Vector3Int(
                character.Position.X,
                character.Position.Y,
                0);

            if (characterChange.Died)
            {
                if (character.Position.BoardId == sceneConfiguration.BoardName)
                {
#if UNITY_EDITOR
                    Debug.Log("Character Dies");
#endif
                    ignoreForHubUpdate.Add(entity);
                    batch.Add(new DespawnTask(entity));
                    batch.Add(new EffectTask(EffectType.Death, position));
                }

                return;
            }
            else if (characterChange.Respawned)
            {
                if (character.Position.BoardId == sceneConfiguration.BoardName)
                {
                    ignoreForHubUpdate.Add(entity);

                    if (isMonster)
                    {
                        batch.Add(new SpawnMonsterTask(entity, position, character.Sprite)
                        {
                            Health     = character.CurrentHealth,
                            Level      = character.Level,
                            Experience = character.Experience
                        });
                    }
                    else
                    {
                        batch.Add(new SpawnPlayerTask(entity, position)
                        {
                            Health     = character.CurrentHealth,
                            Level      = character.Level,
                            Experience = character.Experience
                        });
                    }

                    batch.Add(new EffectTask(EffectType.Spawn, position));
                }

                return;
            }

            switch (characterChange.Decision.DecisionType)
            {
            case DecisionType.Move:
                if (character.Position.BoardId != sceneConfiguration.BoardName)
                {
                    return;
                }

                Vector3[] path = GetPath(characterChange.Path, boardPositionLookUp);
                batch.Add(new FollowPathTask(entity, path));
                break;

            case DecisionType.Portal:
                ignoreForHubUpdate.Add(entity);

                if (character.Position.BoardId != sceneConfiguration.BoardName)
                {
                    batch.Add(new DespawnTask(entity));
                }
                else
                {
                    if (isMonster)
                    {
                        batch.Add(new SpawnMonsterTask(entity, position, character.Sprite)
                        {
                            Health     = character.CurrentHealth,
                            Level      = character.Level,
                            Experience = character.Experience
                        });
                    }
                    else
                    {
                        batch.Add(new SpawnPlayerTask(entity, position)
                        {
                            Health     = character.CurrentHealth,
                            Level      = character.Level,
                            Experience = character.Experience
                        });
                    }
                }

                batch.Add(new EffectTask(EffectType.Portal, position));
                break;

            case DecisionType.Attack:
                if (character.Position.BoardId == sceneConfiguration.BoardName)
                {
                    foreach (var location in characterChange.AttackLocations)
                    {
                        var attackPosition = new Vector3Int()
                        {
                            x = location.X,
                            y = location.Y,
                            z = 0
                        };

                        batch.Add(new EffectTask(EffectType.Attack, attackPosition));
                    }
                }
                break;

            case DecisionType.Equip:
                if (character.Position.BoardId == sceneConfiguration.BoardName && !isMonster)
                {
                    ignoreForHubUpdate.Add(entity);

                    Player player = gameState.PlayerNames[entity];

                    batch.Add(new UpdateInventoryTask(entity)
                    {
                        clothes_changed  = characterChange.ClothesChanged,
                        hat_changed      = characterChange.HatChanged,
                        shoes_changed    = characterChange.ShoesChanged,
                        weapon_changed   = characterChange.WeaponChanged,
                        accesory_changed = characterChange.AccessoryChanged,

                        Top       = player.Clothes?.Sprite,
                        Bottom    = player.Shoes?.Sprite,
                        Head      = player.Hat?.Sprite,
                        Weapon    = character.Weapon?.Sprite,
                        Accessory = player.Accessory?.Sprite
                    });
                    return;
                }
                break;

            case DecisionType.None:
                break;

            default:
                Debug.LogWarningFormat("Unrecognized decision type {0}", characterChange.Decision.DecisionType);
                break;
            }
        }