Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add GameSuggestionEngine class #2978

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

wescopeland
Copy link
Member

@wescopeland wescopeland commented Dec 24, 2024

This PR lays the groundwork for migrating the two game suggestion pages to React. It focuses purely on implementing the back-end logic.

How to test this PR:
Navigate to both of these pages and refresh a bunch of times:

http://localhost:64000/game/1/suggestions
http://localhost:64000/games/suggestions

You'll see pretty-printed JSON output for the table results:
Screenshot 2024-12-24 at 2 46 44 PM

How it works:
The core of this implementation is a new GameSuggestionEngine class, which uses a weighted strategy pattern. Each strategy represents a different way to suggest games to users:

// Example: Set strategies for game-specific suggestions.
if ($this->sourceGame) {
    $this->strategies = [
        [new Strategies\SimilarGameStrategy($this->sourceGame), 50],
        [new Strategies\SharedHubStrategy($this->sourceGame), 20],
        [new Strategies\CommonPlayersStrategy($this->user, $this->sourceGame), 20],
        [new Strategies\SharedAuthorStrategy($this->sourceGame), 10],
    ];
}

Notice that the array values are like [strategy, weight].

Each suggestion strategy implements a GameSuggestionStrategy interface:

interface GameSuggestionStrategy
{
    public function select(): ?Game; // what is being suggested?
    public function reason(): GameSuggestionReason; // why is it being suggested?
    public function reasonContext(): ?GameSuggestionContextData; // is there more to know about the suggestion?
}

This makes it easy to:

  • Add new suggestion types.
  • Adjust suggestion weights.
  • Test each strategy independently.

Here's an example of an implemented strategy to fetch a suggested game from the user's Want to Play Games list:

class WantToPlayStrategy implements GameSuggestionStrategy
{
    public function __construct(
        private readonly User $user
    ) {
    }

    public function select(): ?Game
    {
        return $this->user->gameListEntries()
            ->whereType(UserGameListType::Play)
            ->whereHas('game', function ($query) {
                $query->whereHasPublishedAchievements();
            })
            ->with('game')
            ->inRandomOrder()
            ->first()
            ?->game;
    }

    public function reason(): GameSuggestionReason
    {
        return GameSuggestionReason::WantToPlay;
    }

    public function reasonContext(): ?GameSuggestionContextData
    {
        return null;
    }
}
public function testItSelectsGameFromWantToPlayList(): void
{
    // Arrange
    $user = User::factory()->create();
    $game1 = Game::factory()->create(['Title' => 'Game 1', 'achievements_published' => 6]);
    $game2 = Game::factory()->create(['Title' => 'Game 2', 'achievements_published' => 6]);

    $addGameToListAction = new AddGameToListAction();
    $addGameToListAction->execute($user, $game1, UserGameListType::Play);
    $addGameToListAction->execute($user, $game2, UserGameListType::Play);

    // Act
    $strategy = new WantToPlayStrategy($user);
    $result = $strategy->select();

    // Assert
    $this->assertTrue(in_array($result->id, [$game1->id, $game2->id]));
    $this->assertEquals(GameSuggestionReason::WantToPlay, $strategy->reason());
    $this->assertNull($strategy->reasonContext());
}

All of the strategies are covered by tests.


Next steps:

  • Have the React datatable support a "client-only" mode, where if <N results are returned, all table operations are handled client-side in browser memory.
  • Build out the React versions of these two pages, remove the associated PHP UIs.

@wescopeland wescopeland requested a review from a team December 24, 2024 19:45
Comment on lines +84 to +88
// We won't show the user games they already have 100% completion for.
$masteredGameIds = PlayerGame::whereUserId($this->user->id)
->whereAllAchievementsUnlocked()
->pluck('game_id')
->toArray();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I missed this in the original commit. I noticed when refreshing constantly in prod, I was never suggested a mastered game. This new logic should filter those out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants