Skip to content
New issue

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

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

Already on GitHub? # to your account

Mc/matchmaking poc1 #339

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft

Mc/matchmaking poc1 #339

wants to merge 6 commits into from

Conversation

marcin-cebo
Copy link
Contributor

No description provided.

Please, remove pubnub-matchmaking-kotlinOld module after you rewrite logic that is inside of it.
Last message in a user-related channel will be pointing to current status.
import com.pubnub.kmp.PNFuture

// todo add kdoc
interface User {
Copy link
Contributor

Choose a reason for hiding this comment

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

it's concerning that we're copying and creating yet another implementation of User, this time in com.pubnub.matchmaking. If someone uses Chat SDK are they going to have two incompatible User classes? Should this SDK be an additon to Chat SDK? Or maybe User and Channel should be in some base module, used by both Chat and Matchmaking?

this could use some more thought to find the best solution

package com.pubnub.matchmaking.entities

enum class MatchmakingStatus {
IN_QUEUE,
Copy link
Contributor

Choose a reason for hiding this comment

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

what can a user do with all those statuses?

for example, why do we have an UNKNOWN status? we control this code, so shouldn't all of them be known?
what's the difference between IN_QUEUE and RE_ADDED_TO_QUEUE (from user's perspective)?

can we figure out the minimum list of statuses to make this work?

package com.pubnub.matchmaking.entities

class FindMatchResult(
val result: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

String? what's inside?


fun deleteUser(id: String, soft: Boolean = false): PNFuture<User?>

fun findMatch(userId: String): PNFuture<String>
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems overly complicated, couldn't the whole API be just one method -

@Throws(MatchMakingException::class)
fun findMatch(forUserId: String): PNFuture<MatchResult>

where MatchResult contains the set of users that were matched or the ID of the game or some other means of starting the game or confirming the match?

in case of error you would get an exception with details of the error, in case of success you get the result

why would I want to observe statuses like IN_QUEUE, RE_ADDED_TO_QUEUE etc, it's not useful?

PubNubException("Id is required").asFuture()
} else {
PNFuture { callback ->
CoroutineScope(Dispatchers.Default).launch {
Copy link
Contributor

Choose a reason for hiding this comment

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

don't create a new scope, just use the scope from the MatchMakingRestService object

try {
while (true) {
processMatchmakingQueue()
delay(5000L) // Wait 5 seconds between processing
Copy link
Contributor

Choose a reason for hiding this comment

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

why the 5 second delay? and why 5 seconds?

delay(5000L) // Wait 5 seconds between processing
}
} finally {
processingQueueInProgress = false
Copy link
Contributor

Choose a reason for hiding this comment

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

this will never execute because of the above while loop, but if it did it's not guarded by Mutex (all other access to this variable is guarded)

}

// Create all possible pairs (only including allowed pairs)
private fun createAllPairs(users: List<User>): List<Triple<Int, Int, Double>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

this List<Triple<Int,Int,Double>> is unreadable (it's difficult to say what these ints and doubles represent), consider a better return type


val eloDifference = abs(eloA - eloB)
if (eloDifference > maxEloGap) {
return Double.POSITIVE_INFINITY
Copy link
Contributor

Choose a reason for hiding this comment

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

wouldn't null instead of Double.POSITIVE_INFINITY be more appropriate in this case?


// Greedy pairing: sort by score and select pairs without conflicts.
// Instead of returning MatchmakingPair, we create a MatchGroup that holds a set of users.
private fun pairUsersBySkill(users: List<User>): MatchmakingResult {
Copy link
Contributor

Choose a reason for hiding this comment

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

in general I think this algorithm is wasteful:

  1. why create all pairs when we're only going to use some of them?
  2. and why do it every 5 seconds, even if nothing has changed (no new users are requesting a match)?

instead of running a loop that checks all possibilities every X seconds, I'd explore something more event-based, like:

  1. there is some List<MatchGroup> that is initially empty
  2. a user with some userId and skill calls findMatch()
  3. findMatch iterates over the list of open match groups (i.e. matches that have not started because they don't have the required number of players) and if the skill matches the group's skill level (due to some rules), add the user to the group
  4. if the group has the necessary number of players, inform all users in the group that they have a match (using the callback, or in REST by returning HTTP 200)
  5. if there is no group that matches the player's skill, open a new group with this user and add it to the list of open matches (HTTP is in progress until the group is complete)

So everything would happen in response to a new player requesting a match. The only timer that I'd add would be to support some sort of timeout if the player has been waiting for too long to return with a "no match found" error or something like that.

…otlinx.coroutines.channels.Channel receive/send functionality
}
if (groupToJoin != null) {
// Join the found group.
groupToJoin!!.users.add(user)
Copy link
Contributor

Choose a reason for hiding this comment

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

reorganize the code to avoid using "!!", for example:
remove the groupToJoin variable, do instead: openMatchGroups.firstOrNull { ... }?.let { groupToJoin -> ... } ?: run { // group not found }

Copy link
Contributor

Choose a reason for hiding this comment

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

or just make it a val instead of var so you can use smart cast

private val openMatchGroups = mutableListOf<OpenMatchGroup>()
private val groupsMutex = Mutex()

@Throws(MatchMakingException::class)
Copy link
Contributor

@wkal-pubnub wkal-pubnub Mar 11, 2025

Choose a reason for hiding this comment

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

this doesn't need @throws if it returns PNFuture because the exception is delivered inside the future

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

Successfully merging this pull request may close these issues.

2 participants