Skip to content

Commit

Permalink
Updated CodeTour
Browse files Browse the repository at this point in the history
  • Loading branch information
marcduiker committed Oct 21, 2022
1 parent 9ba30de commit 0a9814e
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 116 deletions.
19 changes: 12 additions & 7 deletions .tours/serverless-websockets-quest.tour
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
221 changes: 112 additions & 109 deletions api/Models/GameState.cs
Original file line number Diff line number Diff line change
@@ -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<string>();
}

[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<string> PlayerNames { get; set; }
public async Task AddPlayerName(string playerName)
{
if (PlayerNames == null)
{
PlayerNames = new List<string> { 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<IPlayer>(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<GameState>();
}
}
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<string>();
}

[JsonProperty("questId")]
public string QuestId { get; set; }

[JsonProperty("phase")]
public string Phase { get; set; }

[JsonProperty("players")]
public List<string> 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<string> { 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<IPlayer>(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<GameState>();
}
}

0 comments on commit 0a9814e

Please sign in to comment.