public static Game Parse(string source) { var jsonGame = JsonConvert.DeserializeObject <JObject>(source); var places = new Dictionary <string, GamePlace>(); foreach (var jsonPlace in GetObject(jsonGame, "places")?.Properties() ?? Enumerable.Empty <JProperty>()) { // get id and description for place var id = jsonPlace.Name; var description = GetString(jsonPlace.Value, "description"); var instructions = GetString(jsonPlace.Value, "instructions"); bool.TryParse(GetString(jsonPlace.Value, "finished") ?? "false", out bool finished); // parse player choices var choices = new Dictionary <GameCommandType, IEnumerable <KeyValuePair <GameActionType, string> > >(); foreach (var jsonChoice in GetObject(jsonPlace.Value, "choices")?.Properties() ?? Enumerable.Empty <JProperty>()) { // parse choice command var choice = (string)jsonChoice.Name; if (!Enum.TryParse(choice, true, out GameCommandType command)) { throw new GameLoaderException($"Illegal value for choice ({choice}) at {jsonChoice.Path}."); } if (jsonChoice.Value is JArray array) { var actions = array.Select(item => { var property = ((JObject)item).Properties().First(); if (!Enum.TryParse(property.Name, true, out GameActionType action)) { throw new GameLoaderException($"Illegal key for action ({property.Name}) at {property.Path}."); } return(new KeyValuePair <GameActionType, string>(action, (string)property.Value)); }).ToArray(); choices[command] = actions; } else { throw new GameLoaderException($"Expected object at {jsonChoice.Value.Path} but found {jsonChoice.Value?.Type.ToString() ?? "null"} instead."); } } var place = new GamePlace(id, description, instructions, finished, choices); places[place.Id] = place; } // ensure there is a start room if (!places.ContainsKey(Game.StartPlaceId)) { places[Game.StartPlaceId] = new GamePlace( Game.StartPlaceId, "No start place is defined for this adventure. Please check your adventure file and try again.", "Please check your adventure file and try again.", false, new Dictionary <GameCommandType, IEnumerable <KeyValuePair <GameActionType, string> > >() ); } return(new Game(places)); // helper functions JObject GetObject(JToken json, string key) { if (json is JObject objOuter) { var token = objOuter[key]; if (token == null) { return(null); } if (token is JObject objInner) { return(objInner); } throw new GameLoaderException($"Expected object at {json.Path}.{key} but found {token?.Type.ToString() ?? "null"} instead."); } else { throw new GameLoaderException($"Expected object at {json.Path} but found {json?.Type.ToString() ?? "null"} instead."); } } string GetString(JToken token, string key, bool required = true) { if (token is JObject obj) { var value = obj[key]; try { return((string)value); } catch { throw new GameLoaderException($"Expected string at {token.Path}.{key} but found {token?.Type.ToString().ToLower() ?? "null"} instead."); } } else if (required) { throw new GameLoaderException($"Expected string at {token.Path} but found {token?.Type.ToString().ToLower() ?? "null"} instead."); } return(null); } }
//--- Methods --- public IEnumerable <AGameResponse> Do(GamePlayer player, GameCommandType command) { var result = new List <AGameResponse>(); // some commands are optional and don't require to be defined for a place var optional = false; switch (command) { case GameCommandType.Describe: case GameCommandType.Help: case GameCommandType.Hint: case GameCommandType.Restart: case GameCommandType.Quit: optional = true; break; } // check if the place has associated actions for the choice GamePlace place = Places[player.PlaceId]; if (place.Choices.TryGetValue(command, out IEnumerable <KeyValuePair <GameActionType, string> > choice)) { foreach (var action in choice) { switch (action.Key) { case GameActionType.Goto: if (!Places.TryGetValue(action.Value, out place)) { throw new GameException($"Cannot find place: '{action.Value}'"); } if (player.PlaceId != place.Id) { player.PlaceId = place.Id; DescribePlace(place); // check if the current place marks the end of the adventure if (place.Finished) { result.Add(new GameResponseFinished()); } } break; case GameActionType.Say: result.Add(new GameResponseSay(action.Value)); break; case GameActionType.Pause: if (!double.TryParse(action.Value, out double delayValue)) { throw new GameException($"Delay must be a number: '{action.Value}'"); } result.Add(new GameResponseDelay(TimeSpan.FromSeconds(delayValue))); break; case GameActionType.Play: result.Add(new GameResponsePlay(action.Value)); break; } } } else if (!optional) { result.Add(new GameResponseNotUnderstood()); } switch (command) { case GameCommandType.Describe: DescribePlace(place); break; case GameCommandType.Help: result.Add(new GameResponseSay(place.Instructions)); break; case GameCommandType.Hint: // hints are optional; nothing else to do by default break; case GameCommandType.Restart: if ((choice == null) || !choice.Any(c => c.Key == GameActionType.Goto)) { place = Places[Game.StartPlaceId]; player.PlaceId = place.Id; } DescribePlace(place); break; case GameCommandType.Quit: result.Add(new GameResponseBye()); break; } return(result); // helper functions void DescribePlace(GamePlace current) { if ((current.Description != null) && (current.Instructions != null)) { result.Add(new GameResponseSay(current.Description)); result.Add(new GameResponseSay(current.Instructions)); } else if (current.Description != null) { result.Add(new GameResponseSay(current.Description)); } else if (current.Instructions != null) { result.Add(new GameResponseSay(current.Instructions)); } } }