Firebase: How to match opponents in a game?

2020-02-04 06:39发布

I'm implementing a social chess game. Every user can create a new game, and they'll wait until the system will find an opponent for them.

When user creates a game, they specify constraints: color they'd like to play, and opponent's minimal chess rating.

Opponents can either match or not match. For example, the following two opponents will match:

// User 1 with rating 1700              // User 2 with rating 1800
// creates this game                    // creates this game
game: {                                 game: { 
  color: 'white',                         minRating: 1650
  minRating: 1600                       }
}                                       // User did not specify a preferred color,
                                        // meaning they do not care which color to play

So, if User 1 is the first user in the system, and created their game, they'll wait. Once User 2 creates their game, they should be matched immediately with User 1.

On the other side, the following two opponents won't match, because they both want to play white. In this case, both should wait until someone else creates a game with color: 'black' (or color not specified), and minRating that would match the requirements.

// User 1 with rating 1700              // User 2 with rating 1800
// creates this game                    // creates this game
game: {                                 game: { 
  color: 'white',                         color: 'white'  
  minRating: 1600                         minRating: 1650
}                                       }

My concerns related to scenarios where thousands of users creates new games at the same time. How do I make sure that I match opponents without creating deadlocks? i.e. how do I prevent scenarios when User 1, User 2, and User 3 are trying to find an opponent at the same time, and their matching algorithms return User 99. How do I recover from this scenario, assigning User 99 to only one of them?

How would you use the power of Firebase to implement such a matching system?

2条回答
该账号已被封号
2楼-- · 2020-02-04 06:48

The obvious choice for a starting point would be the color, since this is an exclusive requirement. The others seem more like weighted results, so those could simply increment or decrement the weight.

Utilize priorities for min/max ranges, and keep each in a separate "index". Then grab the matches for each and create a union. Consider this structure:

/matches
/matches/colors/white/$user_id
/matches/ranking/$user_id (with a priority equal to ranking)
/matches/timezones/$user_id (with a priority of the GMT relationship)

Now to query, I would simply grab the matches in each category and rank them by the number of matches. I can start with colors, because this presumably isn't an optional or relative rating:

var rootRef = new Firebase('.../matches');

var VALUE = {
   "rank": 10, "timezone": 5, "color": 0
}

var matches = []; // a list of ids sorted by weight
var weights = {}; // an index of ids to weights

var colorRef = rootRef.child('colors/black');
colorRef.on('child_added', addMatch);
colorRef.child('colors/black').on('child_removed', removeMatch);

var rankRef = rootRef.child('ranking').startAt(minRank).endAt(maxRank);
rankRef.on('child_added', addWeight.bind(null, VALUE['rank']));
rankRef.on('child_removed', removeWeight.bind(null, VALUE['rank']));

var tzRef = ref.child('timezone').startAt(minTz).endAt(maxTz);
tzRef.on('child_added', addWeight.bind(null, VALUE['timezone']));
tzRef.on('child_removed', removeWeight.bind(null, VALUE['timezone']));

function addMatch(snap) {
   var key = snap.name();
   weights[key] = VALUE['color'];
   matches.push(key);
   matches.sort(sortcmp);
}

function removeMatch(snap) {
   var key = snap.name();
   var i = matches.indexOf(key);
   if( i > -1 ) { matches.splice(i, 1); }
   delete weights[key]; 
}

function addWeight(amt, snap) {
   var key = snap.name();
   if( weights.hasOwnProperty(key) ) {
      weights[key] += amt;
      matches.sort(sortcmp);
   }
}

function removeWeight(amt, snap) {
   var key = snap.name();
   if( weights.hasOwnProperty(key) ) {
      weights[key] -= amt;
      matches.sort(sortcmp);
   }
}

function sortcmp(a,b) {
   var x = weights[a];
   var y = weights[b];
   if( x === y ) { return 0; }
   return x > y? 1 : -1;
}

Okay, now I've given what everyone asks for in this use case--how to create a rudimentary where clause. However, the appropriate answer here is that searches should be performed by a search engine. This is no simple where condition. This is a weighted search for the best matches, because fields like color are not optional or simply the best match, while others--ranking maybe--are the closest match in either direction, while some simply affect the quality of the match.

Check out flashlight for a simple ElasticSearch integration. With this approach, you should be able to take advantage of ES's great weighting tools, dynamic sorting, and everything else you need to conduct a proper matching algorithm.

Regarding deadlocks. I would not put too much focus here until you have hundreds of transactions per second (i.e. hundreds of thousands of users competing for matches). Split out the path where we will write to accept a join and do a transaction to ensure only one person succeeds in obtaining it. Keep it separate from the read data so that the lock on that path won't slow down processing. Keep the transaction to a minimal size (a single field if possible).

查看更多
虎瘦雄心在
3楼-- · 2020-02-04 06:56

It is a challenging task in NoSQL environment especially if you want to match multiple fields

in your case, I would setup a simple index by color and within the color I would store the reference to the game with priority set to minRating. That way you can query the games by the prefered colour with the priority of minRating.

indexes: {
  color:{
     white:{
        REF_WITH_PRIORITY_TO_RATING: true
     },
     black:{
        REF_WITH_PRIORITY_TO_RATING: true
     }
  }
}

if you want to get info whenever the match opens the game:

ref = new(Firebase)('URL');
query =ref.child('color_index/white/').startAt(minPriority);
query.on('child_added',function(snapshot){
  //here is your new game matching the filter
});

This, however, it would get more complex if you introduce multiple fields for filtering the games for example dropRate, timeZone, 'gamesPlayed' etc... In this case, you can nest the indexes deeper:

indexes: {
  GMT0: {
    color:{
       white:{
          REF_WITH_PRIORITY_TO_RATING: true
       },
       black:{
          REF_WITH_PRIORITY_TO_RATING: true
       },
  }
  GMT1: {
       // etc
  }
}
查看更多
登录 后发表回答