From 0a9814e8d9a42a0a83d26188d31650061d741067 Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Fri, 21 Oct 2022 13:06:15 +0300 Subject: [PATCH] Updated CodeTour --- .tours/serverless-websockets-quest.tour | 19 +- api/Models/GameState.cs | 221 ++++++++++++------------ 2 files changed, 124 insertions(+), 116 deletions(-) diff --git a/.tours/serverless-websockets-quest.tour b/.tours/serverless-websockets-quest.tour index 0c6dd9a..47ef9c2 100644 --- a/.tours/serverless-websockets-quest.tour +++ b/.tours/serverless-websockets-quest.tour @@ -77,26 +77,31 @@ "description": "### Player -> Ably\r\n\r\nOnce a `Player` entity is initialized, a message will be published to an Ably channel. The players who have joined the quest are subscribed to this channel and will receive a message that a new player has joined.", "line": 39 }, + { + "file": "api/Models/Publisher.cs", + "description": "### Publisher\r\n\r\nThe final step of game logic functionality in the API is publishing messages to the players that have joined the quest. Since several classes need access to this functionality, I've wrapped it in a `Publisher` class for ease of use.", + "line": 6 + }, { "file": "api/Models/GameState.cs", - "description": "### `GameState` entity function\r\n\r\nThe `GameState` entity function is responsible for maintaining the state of the game, such as the quest ID, the player names, and the game phase (start, character selection, play, end).", + "description": "### GameState entity function\r\n\r\nThe `GameState` entity function is responsible for maintaining the state of the game, such as the quest ID, the player names, and the game phase (start, character selection, play, end).", "line": 12 }, { "file": "api/Models/GameState.cs", "description": "### GameState -> Ably\r\n\r\nOnce the QuestId and Phase have been set a message is published to advance the player to the next game phase.", - "line": 31 - }, - { - "file": "api/Models/Publisher.cs", - "description": "### Publisher\r\n\r\nThe final step of game logic functionality in the API is publishing messages to the players that have joined the quest. Since several classes need access to this functionality, I've wrapped it in a `Publisher` class for ease of use.", - "line": 6 + "line": 38 }, { "file": "src/stores/index.ts", "description": "### WebSocket connection\r\n\r\nThe client-side is subscribed to the messages published via the API using the realtime Ably client that is based on WebSockets. Based on the type of message received, the game progresses to the next phase, and local player state is updated. So, even though this is a turn-based game, updates in the API result in realtime communication with the players to update their local player state.\r\n\r\nThe clients require a connection to Ably to receive messages in realtime. The `createRealtimeConnection` function is called when players start a new quest or join a quest.", "line": 173 }, + { + "file": "src/stores/index.ts", + "description": "## Attach to channel\n\nThe player is attaching to the quest channel that will be used for receiving the messages from Ably.", + "line": 202 + }, { "file": "src/stores/index.ts", "description": "### Subscribe\r\n\r\nThe player is subscribing to specific messages that control the game play.", diff --git a/api/Models/GameState.cs b/api/Models/GameState.cs index bb813bf..d8f5ed6 100644 --- a/api/Models/GameState.cs +++ b/api/Models/GameState.cs @@ -1,109 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Newtonsoft.Json; - -namespace AblyLabs.ServerlessWebsocketsQuest.Models -{ - [JsonObject(MemberSerialization.OptIn)] - public class GameState : IGameState - { - private const int NumberOfPlayers = 4; - private readonly Publisher _publisher; - - public GameState(Publisher publisher) - { - _publisher = publisher; - QuestId = string.Empty; - Phase = string.Empty; - PlayerNames = new List(); - } - - [JsonProperty("questId")] - public string QuestId { get; set; } - public async Task InitGameState(string[] gameStateFields) - { - QuestId = gameStateFields[0]; - Phase = gameStateFields[1]; - await _publisher.PublishUpdatePhase(QuestId, Phase); - } - - [JsonProperty("phase")] - public string Phase { get; set; } - public async Task UpdatePhase(string phase) - { - Phase = phase; - await _publisher.PublishUpdatePhase(QuestId, Phase); - } - - [JsonProperty("players")] - public List PlayerNames { get; set; } - public async Task AddPlayerName(string playerName) - { - if (PlayerNames == null) - { - PlayerNames = new List { playerName }; - } - else - { - PlayerNames.Add(playerName); - } - - if (IsPartyComplete) - { - await UpdatePhase(GamePhases.Play); - await Task.Delay(1000); - await AttackByMonster(); - } - } - - public async Task RemovePlayerName(string playerName) - { - PlayerNames.Remove(playerName); - - if (PlayerNames.Count == 0) - { - var teamHasWon = false; - await _publisher.PublishUpdatePhase(QuestId, GamePhases.End, teamHasWon); - } - } - - private async Task AttackByMonster() - { - var playerAttacking = CharacterClassDefinitions.Monster.Name; - var playerUnderAttack = GetRandomPlayerName(); - var damage = CharacterClassDefinitions.GetDamageFor(CharacterClassDefinitions.Monster.CharacterClass); - await _publisher.PublishPlayerAttacking(QuestId, playerAttacking, playerUnderAttack, damage); - await Task.Delay(1000); - var playerEntityId = new EntityId(nameof(Player), Player.GetEntityId(QuestId, playerUnderAttack)); - Entity.Current.SignalEntity(playerEntityId, proxy => proxy.ApplyDamage(damage)); - await Task.Delay(1000); - var nextPlayerName = GetNextPlayerName(CharacterClassDefinitions.Monster.Name); - await _publisher.PublishPlayerTurnAsync(QuestId, $"Next turn: {nextPlayerName}", nextPlayerName); - } - - public string GetNextPlayerName(string currentPlayerName) - { - var currentIndex = PlayerNames.FindIndex(0, PlayerNames.Count, p => p == currentPlayerName); - string nextPlayer = currentIndex == PlayerNames.Count - 1 ? PlayerNames[0] : PlayerNames[currentIndex + 1]; - - return nextPlayer; - } - - public bool IsPartyComplete => PlayerNames.Count == NumberOfPlayers; - - public string GetRandomPlayerName() - { - var playerNamesWithoutMonster = PlayerNames.Where(p => p != CharacterClassDefinitions.Monster.Name).ToList(); - var index = new Random().Next(0, playerNamesWithoutMonster.Count); - return playerNamesWithoutMonster[index]; - } - - [FunctionName(nameof(GameState))] - public static Task Run([EntityTrigger] IDurableEntityContext ctx) - => ctx.DispatchAsync(); - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Newtonsoft.Json; + +namespace AblyLabs.ServerlessWebsocketsQuest.Models +{ + [JsonObject(MemberSerialization.OptIn)] + public class GameState : IGameState + { + private const int NumberOfPlayers = 4; + private readonly Publisher _publisher; + + public GameState(Publisher publisher) + { + _publisher = publisher; + QuestId = string.Empty; + Phase = string.Empty; + PlayerNames = new List(); + } + + [JsonProperty("questId")] + public string QuestId { get; set; } + + [JsonProperty("phase")] + public string Phase { get; set; } + + [JsonProperty("players")] + public List PlayerNames { get; set; } + + public async Task InitGameState(string[] gameStateFields) + { + QuestId = gameStateFields[0]; + Phase = gameStateFields[1]; + await _publisher.PublishUpdatePhase(QuestId, Phase); + } + + public async Task UpdatePhase(string phase) + { + Phase = phase; + await _publisher.PublishUpdatePhase(QuestId, Phase); + } + + public async Task AddPlayerName(string playerName) + { + if (PlayerNames == null) + { + PlayerNames = new List { playerName }; + } + else + { + PlayerNames.Add(playerName); + } + + if (IsPartyComplete) + { + await UpdatePhase(GamePhases.Play); + await Task.Delay(1000); + await AttackByMonster(); + } + } + + public async Task RemovePlayerName(string playerName) + { + PlayerNames.Remove(playerName); + + if (PlayerNames.Count == 0) + { + var teamHasWon = false; + await _publisher.PublishUpdatePhase(QuestId, GamePhases.End, teamHasWon); + } + } + + private async Task AttackByMonster() + { + var playerAttacking = CharacterClassDefinitions.Monster.Name; + var playerUnderAttack = GetRandomPlayerName(); + var damage = CharacterClassDefinitions.GetDamageFor(CharacterClassDefinitions.Monster.CharacterClass); + await _publisher.PublishPlayerAttacking(QuestId, playerAttacking, playerUnderAttack, damage); + await Task.Delay(1000); + var playerEntityId = new EntityId(nameof(Player), Player.GetEntityId(QuestId, playerUnderAttack)); + Entity.Current.SignalEntity(playerEntityId, proxy => proxy.ApplyDamage(damage)); + await Task.Delay(1000); + var nextPlayerName = GetNextPlayerName(CharacterClassDefinitions.Monster.Name); + await _publisher.PublishPlayerTurnAsync(QuestId, $"Next turn: {nextPlayerName}", nextPlayerName); + } + + public string GetNextPlayerName(string currentPlayerName) + { + var currentIndex = PlayerNames.FindIndex(0, PlayerNames.Count, p => p == currentPlayerName); + string nextPlayer = currentIndex == PlayerNames.Count - 1 ? PlayerNames[0] : PlayerNames[currentIndex + 1]; + + return nextPlayer; + } + + public bool IsPartyComplete => PlayerNames.Count == NumberOfPlayers; + + public string GetRandomPlayerName() + { + var playerNamesWithoutMonster = PlayerNames.Where(p => p != CharacterClassDefinitions.Monster.Name).ToList(); + var index = new Random().Next(0, playerNamesWithoutMonster.Count); + return playerNamesWithoutMonster[index]; + } + + [FunctionName(nameof(GameState))] + public static Task Run([EntityTrigger] IDurableEntityContext ctx) + => ctx.DispatchAsync(); + } +}