Skip to content

Commit

Permalink
Make RandomLevelsCategory use seed from game (#323)
Browse files Browse the repository at this point in the history
Closes #122

# Why
Previously, we were using a globally shared instance of `Random` when
calculating the random order of levels. This caused levels to appear
multiple times in the list and caused others to get potentially missed
entirely.

By using a consistent seed we can negate both of those problems and
ensure the full list gets hit consistently.

# How
To fix, introduce the client's `seed` parameter as an available
parameter to use in the filter settings. We can then use this to create
a new `Random` object on each call of `GetRandomLevel()` with the new
seed.

# Remarks
This will require a change in `refresh-web` to make the website generate
a random seed, since if we do not find a seed value server-side we
simply use `0`.
  • Loading branch information
jvyden authored Jan 2, 2024
2 parents 55465b6 + 54f71cc commit 86ae4a2
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 11 deletions.
12 changes: 8 additions & 4 deletions Refresh.GameServer/Database/GameDatabaseContext.Levels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,16 @@ public DatabaseList<GameLevel> GetNewestLevels(int count, int skip, GameUser? us
.OrderByDescending(l => l.PublishDate), skip, count);

[Pure]
public DatabaseList<GameLevel> GetRandomLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings) =>
new(this.GetLevelsByGameVersion(levelFilterSettings.GameVersion)
public DatabaseList<GameLevel> GetRandomLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings)
{
Random random = new(levelFilterSettings.Seed ?? 0);

return new DatabaseList<GameLevel>(this.GetLevelsByGameVersion(levelFilterSettings.GameVersion)
.FilterByLevelFilterSettings(user, levelFilterSettings)
.AsEnumerable()
.OrderBy(_ => Random.Shared.Next()), skip, count);

.OrderBy(_ => random.Next()), skip, count);
}

// TODO: reduce code duplication for getting most of x
[Pure]
public DatabaseList<GameLevel> GetMostHeartedLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Levels.Categories;

namespace Refresh.GameServer.Endpoints.Game.Levels.FilterSettings;

Expand Down Expand Up @@ -45,6 +46,12 @@ public class LevelFilterSettings
/// </summary>
public TokenGame GameVersion;

/// <summary>
/// The seed used for lucky dip/random levels.
/// </summary>
/// <seealso cref="RandomLevelsCategory"/>
public int? Seed;

public LevelFilterSettings(TokenGame game)
{
this.GameVersion = game;
Expand Down Expand Up @@ -126,5 +133,11 @@ public LevelFilterSettings(RequestContext context, TokenGame game) : this(game)
this.Labels ??= new string[1];
this.Labels[0] = labelFilter0;
}

string? seedStr = context.QueryString.Get("seed");
if (seedStr != null && int.TryParse(seedStr, out int seed))
{
this.Seed = seed;
}
}
}
30 changes: 23 additions & 7 deletions RefreshTests.GameServer/Tests/Levels/LevelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,32 @@ public void SlotsRandom()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();
GameLevel level = context.CreateLevel(user);

// 3 levels to test with
GameLevel level1 = context.CreateLevel(user);
GameLevel level2 = context.CreateLevel(user);
GameLevel level3 = context.CreateLevel(user);

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);

HttpResponseMessage message = client.GetAsync($"/lbp/slots/lbp2luckydip").Result;
Assert.That(message.StatusCode, Is.EqualTo(OK));

SerializedMinimalLevelList result = message.Content.ReadAsXML<SerializedMinimalLevelList>();
Assert.That(result.Items, Has.Count.EqualTo(1));
Assert.That(result.Items.First().LevelId, Is.EqualTo(level.LevelId));
void TestSeed(GameLevel expectedLevel, int seed)
{
// Iterate through a bunch of times to ensure it's deterministic
for (int i = 0; i < 10; i++)
{
HttpResponseMessage message = client.GetAsync($"/lbp/slots/lbp2luckydip?seed={seed}").Result;
Assert.That(message.StatusCode, Is.EqualTo(OK));

SerializedMinimalLevelList result = message.Content.ReadAsXML<SerializedMinimalLevelList>();
Assert.That(result.Items, Has.Count.EqualTo(3));
Assert.That(result.Items.First().LevelId, Is.EqualTo(expectedLevel.LevelId));
}
}

TestSeed(level1, 69420);
TestSeed(level2, 1);
TestSeed(level3, 2);
TestSeed(level3, -2);
}

[Test]
Expand Down

0 comments on commit 86ae4a2

Please sign in to comment.