Skip to content

Commit

Permalink
Merge pull request #31 from planetarium/bugfix/issue-28
Browse files Browse the repository at this point in the history
Fix AvatarAddrAndScoresWithRank edge case
  • Loading branch information
ipdae authored Dec 31, 2024
2 parents 9c21db3 + 0661830 commit 814f4d5
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 353 deletions.
27 changes: 27 additions & 0 deletions ArenaService.Tests/ArenaService.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ArenaService\ArenaService\ArenaService.csproj" />
</ItemGroup>

</Project>
130 changes: 130 additions & 0 deletions ArenaService.Tests/ArenaWorkerTest.cs

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions ArenaService.Tests/AvatarAddressAndScoreTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Libplanet.Crypto;

namespace ArenaService.Tests;

public class AvatarAddressAndScoreTest
{
[Fact]
public void Except_List()
{
var address = new PrivateKey().Address;
var address2 = new PrivateKey().Address;
var addressAndScore = new AvatarAddressAndScore(address, 100);
var addressAndScore2 = new AvatarAddressAndScore(address2, 100);
var updated = new AvatarAddressAndScore(address, 200);

// not equal because score is different
Assert.NotEqual(addressAndScore, updated);
// not equal because address is different
Assert.NotEqual(addressAndScore, addressAndScore2);

var prev = new List<AvatarAddressAndScore>
{
addressAndScore,
addressAndScore2,
};
var next = new List<AvatarAddressAndScore>
{
updated,
addressAndScore2
};
var excepted = Assert.Single(next.Except(prev));
Assert.Equal(updated.AvatarAddr, excepted.AvatarAddr);
Assert.Equal(updated.Score, excepted.Score);
Assert.Equal(updated, excepted);
}
}
6 changes: 6 additions & 0 deletions ArenaService/ArenaService.sln
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArenaService", "ArenaServic
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NineChronicles.RPC.Shared", "..\NineChronicles.RPC.Shared\NineChronicles.RPC.Shared\NineChronicles.RPC.Shared.csproj", "{C936C300-10D8-4A70-8815-67F16CEC6A0A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArenaService.Tests", "..\ArenaService.Tests\ArenaService.Tests.csproj", "{B8F66D71-6DAB-4CF3-B5DA-E35FAAB393F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -18,5 +20,9 @@ Global
{C936C300-10D8-4A70-8815-67F16CEC6A0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C936C300-10D8-4A70-8815-67F16CEC6A0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C936C300-10D8-4A70-8815-67F16CEC6A0A}.Release|Any CPU.Build.0 = Release|Any CPU
{B8F66D71-6DAB-4CF3-B5DA-E35FAAB393F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8F66D71-6DAB-4CF3-B5DA-E35FAAB393F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F66D71-6DAB-4CF3-B5DA-E35FAAB393F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F66D71-6DAB-4CF3-B5DA-E35FAAB393F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using Libplanet.Crypto;
using Nekoyume.Model.State;

namespace ArenaService;

public struct ArenaParticipant
public struct ArenaParticipantStruct
{
public Address AvatarAddr { get; set; }
public int Score { get; set; }
Expand All @@ -15,11 +14,11 @@ public struct ArenaParticipant
public string NameWithHash { get; set; } = "";
public int Level { get; set; }

public ArenaParticipant()
public ArenaParticipantStruct()
{
}

public ArenaParticipant(
public ArenaParticipantStruct(
Address avatarAddr,
int score,
int rank,
Expand Down
20 changes: 10 additions & 10 deletions ArenaService/ArenaService/ArenaParticipantType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,44 @@

namespace ArenaService;

public class ArenaParticipantType : ObjectGraphType<ArenaParticipant>
public class ArenaParticipantType : ObjectGraphType<ArenaParticipantStruct>
{
public ArenaParticipantType()
{
Field<NonNullGraphType<AddressType>>(
nameof(ArenaParticipant.AvatarAddr),
nameof(ArenaParticipantStruct.AvatarAddr),
description: "Address of avatar.",
resolve: context => context.Source.AvatarAddr);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.Score),
nameof(ArenaParticipantStruct.Score),
description: "Arena score of avatar.",
resolve: context => context.Source.Score);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.Rank),
nameof(ArenaParticipantStruct.Rank),
description: "Arena rank of avatar.",
resolve: context => context.Source.Rank);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.WinScore),
nameof(ArenaParticipantStruct.WinScore),
description: "Score for victory.",
resolve: context => context.Source.WinScore);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.LoseScore),
nameof(ArenaParticipantStruct.LoseScore),
description: "Score for defeat.",
resolve: context => context.Source.LoseScore);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.Cp),
nameof(ArenaParticipantStruct.Cp),
description: "Cp of avatar.",
resolve: context => context.Source.Cp);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.PortraitId),
nameof(ArenaParticipantStruct.PortraitId),
description: "Portrait icon id.",
resolve: context => context.Source.PortraitId);
Field<NonNullGraphType<IntGraphType>>(
nameof(ArenaParticipant.Level),
nameof(ArenaParticipantStruct.Level),
description: "Level of avatar.",
resolve: context => context.Source.Level);
Field<NonNullGraphType<StringGraphType>>(
nameof(ArenaParticipant.NameWithHash),
nameof(ArenaParticipantStruct.NameWithHash),
description: "Name of avatar.",
resolve: context => context.Source.NameWithHash);
}
Expand Down
69 changes: 63 additions & 6 deletions ArenaService/ArenaService/ArenaWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,20 @@ public async Task PrepareArenaParticipants()
}

var tip = _rpcClient.Tip!;
var blockIndex = tip.Index;
var currentRoundData = await _rpcClient.GetRoundData(tip, cancellationToken);
var participants = await _rpcClient.GetArenaParticipantsState(tip, currentRoundData, cancellationToken);
var championshipId = currentRoundData.ChampionshipId;
var round = currentRoundData.Round;
var cacheKey = $"{currentRoundData.ChampionshipId}_{currentRoundData.Round}";
var scoreCacheKey = $"{cacheKey}_scores";
var prevAddrAndScores = await _service.GetAvatarAddrAndScores(scoreCacheKey);
var prevArenaParticipants = await _service.GetArenaParticipantsAsync(cacheKey);
var expiry = TimeSpan.FromMinutes(5);
if (participants is null)
{
await _service.SetArenaParticipantsAsync(cacheKey, new List<ArenaParticipant>(), expiry);
_logger.LogInformation("[ArenaParticipantsWorker] participants({CacheKey}) is null. set empty list", cacheKey);
await _service.SetArenaParticipantsAsync(cacheKey, new List<ArenaParticipantStruct>(), expiry);
_logger.LogInformation("[ArenaParticipantsWorker] participants({CacheKey}) is null. set empty list on {BlockIndex}", cacheKey, blockIndex);
return;
}

Expand All @@ -79,14 +82,68 @@ public async Task PrepareArenaParticipants()
// 이전상태의 아바타 주소, 점수를 비교해서 추가되거나 점수가 변경된 대상만 찾음
var updatedAddressAndScores = avatarAddrAndScores.Except(prevAddrAndScores).ToList();
// 전체목록의 랭킹 순서 처리
var avatarAddrAndScoresWithRank = _rpcClient.AvatarAddrAndScoresWithRank(avatarAddrAndScores);
var avatarAddrAndScoresWithRank = AvatarAddrAndScoresWithRank(avatarAddrAndScores);
// 전체목록의 ArenaParticipant 업데이트
var result = await _rpcClient.GetArenaParticipants(tip, updatedAddressAndScores.Select(i => i.AvatarAddr).ToList(), avatarAddrAndScoresWithRank, prevArenaParticipants, cancellationToken);
var tuple = await _rpcClient.GetArenaParticipants(tip, championshipId, round, updatedAddressAndScores.Select(i => i.AvatarAddr).ToList(), avatarAddrAndScoresWithRank, prevArenaParticipants, cancellationToken);
// 캐시 업데이트
await _service.SetArenaParticipantsAsync(cacheKey, result, expiry);
await _service.SetArenaParticipantsAsync(cacheKey, tuple.Item1, expiry);
await _service.SetSeasonAsync(cacheKey, expiry);
await _service.SetAvatarAddrAndScores(scoreCacheKey, avatarAddrAndScores, expiry);
sw.Stop();
_logger.LogInformation("[ArenaParticipantsWorker]Set Arena Cache[{CacheKey}]: {Elapsed}", cacheKey, sw.Elapsed);
_logger.LogInformation("[ArenaParticipantsWorker]Set Arena Cache[{CacheKey}] on {BlockIndex}/{LatestBattleBlockIndex}: {Elapsed}", cacheKey, blockIndex, tuple.Item2, sw.Elapsed);
}


/// <summary>
/// Retrieves the avatar addresses and scores with ranks for a given list of avatar addresses, current round data, and world state.
/// </summary>
/// <param name="avatarAddrAndScores">Ths list of avatar address and score tuples.</param>
/// <returns>The list of avatar addresses, scores, and ranks.</returns>
public static List<ArenaScoreAndRank> AvatarAddrAndScoresWithRank(List<AvatarAddressAndScore> avatarAddrAndScores)
{
if (avatarAddrAndScores.Count == 0)
{
return new List<ArenaScoreAndRank>();
}

if (avatarAddrAndScores.Count == 1)
{
var score = avatarAddrAndScores.Single();
return [new ArenaScoreAndRank(score.AvatarAddr, score.Score, 1)];
}

var orderedTuples = avatarAddrAndScores
.OrderByDescending(tuple => tuple.Score)
.ThenBy(tuple => tuple.AvatarAddr)
.ToList();

var avatarAddrAndScoresWithRank = new List<ArenaScoreAndRank>();
while (orderedTuples.Count > 0)
{
// 동점자를 찾기위해 기준 점수 설정
var currentScore = orderedTuples.First().Score;
var groupSize = 0;
var targets = new List<AvatarAddressAndScore>();
foreach (var tuple in orderedTuples)
{
if (currentScore == tuple.Score)
{
groupSize++;
targets.Add(tuple);
}
else
{
break;
}
}

// 순위는 기존 상위권 순위 + 동점자의 숫자
var rank = avatarAddrAndScoresWithRank.Count + groupSize;
avatarAddrAndScoresWithRank.AddRange(targets.Select(tuple => new ArenaScoreAndRank(tuple.AvatarAddr, tuple.Score, rank)));
// 다음 순위 설정을 위해 이번 그룹 숫자만큼 삭제
orderedTuples.RemoveRange(0, groupSize);
}

return avatarAddrAndScoresWithRank;
}
}
4 changes: 2 additions & 2 deletions ArenaService/ArenaService/IRedisArenaParticipantsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ namespace ArenaService;

public interface IRedisArenaParticipantsService
{
Task<List<ArenaParticipant>> GetArenaParticipantsAsync(string key);
Task SetArenaParticipantsAsync(string key, List<ArenaParticipant> value, TimeSpan? expiry = null);
Task<List<ArenaParticipantStruct>> GetArenaParticipantsAsync(string key);
Task SetArenaParticipantsAsync(string key, List<ArenaParticipantStruct> value, TimeSpan? expiry = null);
Task<string> GetSeasonKeyAsync();
Task SetSeasonAsync(string value, TimeSpan? expiry = null);
Task<List<AvatarAddressAndScore>> GetAvatarAddrAndScores(string key);
Expand Down
8 changes: 4 additions & 4 deletions ArenaService/ArenaService/RedisArenaParticipantsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ public class RedisArenaParticipantsService(IConnectionMultiplexer redis)

private readonly IDatabase _db = redis.GetDatabase();

public async Task<List<ArenaParticipant>> GetArenaParticipantsAsync(string key)
public async Task<List<ArenaParticipantStruct>> GetArenaParticipantsAsync(string key)
{
RedisValue result = await _db.StringGetAsync(key);
if (result.IsNull)
{
return new List<ArenaParticipant>();
return new List<ArenaParticipantStruct>();
}

return JsonSerializer.Deserialize<List<ArenaParticipant>>(result.ToString())!;
return JsonSerializer.Deserialize<List<ArenaParticipantStruct>>(result.ToString())!;
}

public async Task SetArenaParticipantsAsync(string key, List<ArenaParticipant> value, TimeSpan? expiry = null)
public async Task SetArenaParticipantsAsync(string key, List<ArenaParticipantStruct> value, TimeSpan? expiry = null)
{
var serialized = JsonSerializer.Serialize(value);
await _db.StringSetAsync(key, serialized, expiry);
Expand Down
Loading

0 comments on commit 814f4d5

Please sign in to comment.