项目作者: fictorial

项目描述 :
A scalable backend for real-time matchplay games
高级语言: JavaScript
项目地址: git://github.com/fictorial/dueling-monkeys.git
创建时间: 2017-11-03T20:57:40Z
项目社区:https://github.com/fictorial/dueling-monkeys

开源协议:

下载


Dueling Monkeys 🐵 🕹 🐵

A scalable backend for real-time matchplay games.

  • transparent/automatic user account signup
  • automatic matchmaking (“automatching”)
  • real-time match event relay between game clients
  • rules-of-play agnostic
  • match winners determined by unanimous vote
  • stats per player per rules including Elo rating
  • virtual coins as currency for bets
  • in-app purchase receipt verification for replenishing virtual coins
  • less than 400 lines of code; dive in

Users 🙋

  • a user is a human that uses a client app to talk to a Dueling Monkeys
    deployment over the Internet in order to play matches with other human users

Matches

  • a contest between 2 users
    • a user in the context of a match is a “player”
  • governed by “rules of play”
  • has an associated bet or wager in virtual coins
  • cannot end in a tie or stalemate (at this time)
  • real-time in that should finish within minutes (c.f. asynchronous play over days)
  • winner earns loser’s bet

Coins 💰

  • each user has a number of virtual coins
  • to play a match, each player wagers or bets the same number of coins
  • the winner of the match earns the loser’s coins
  • if a user runs out of coins, they can purchase more via in-app purchase

Player Ratings 📈

  • each user has a rating per rules of play
  • the rating uses the Elo rating system

Rules of Play 📖

  • the backend does not care what rules of play govern a match
  • it does bookkeeping, matchmaking, and message exchange
  • it’s up to the clients to manage match state based on relayed match events

Matchmaking 🤝

  • matchmaking is between random players with the same desired rules of play and
    bet amount
  • this is called “automatching”
  • automatching does not involve relative skill levels but is random
  • direct matches are unsupported to thwart cheating
    • later: simply don’t update player ratings for direct matches
  • in the case where rules of play implementations change due to bug fix, etc.
    • the clients should be backwards compatible with older rules of play
      implementation versions as not all clients will upgrade their client app
      and thus two clients could have different implementations otherwise
    • after the match starts, the clients should exchange the version number of
      their rules of play implementation so that they are speaking the same
      language

Matchplay 🌎

  • once in a match, clients exchange messages through the Dueling Monkeys
    deployment
  • these messages can be anything, as long as they are JSON encodable
  • clients can disconnect and return (e.g. on mobile), which is called “presence”
    • there is no store-and-forward of match events
    • thus, clients will need to handle either forwarding state one to the
      other, or pausing the game when the opponent goes offline (presence)

Refereeing, Voting 🗳

  • clients are to govern the state of the match given the rules of play
  • clients are only matched with others using the same rules of play
  • putting clients in charge of match state could lead to cheating
    • typically, the server accepts client input and is authoritative on match
      state
  • however, our clients vote for the winner of a match
    • only a unanimous vote causes the match to end with a normal outcome
    • a disagreement on winner causes the match to be cancelled with a
      “conflict” outcome; no rating updates; no coins won/lost
    • voting makes the backend reusable across many types of games and
      lets clients support offline play (e.g. pass and play, Bluetooth, etc.)
      without implementing rules of play logic on the client and server
    • voting this way is a mindset change vs server authoritative
      • you may of course fork this project and interpret exchanged messages
        between clients of a match should you desire server authority

Trolls, Flagging 😈 🚩

  • only a match with a normal outcome causes coins to be transferred and
    player ratings to be updated, thereby deincentivizing cheating
  • trolls could just disappear if they are about to lose; this is what
    “flagging” is for.
  • a client can flag the opponent in a match for any reason
    • e.g. the opponent’s time offline (presence) exceeds some threshold
  • this cancels the match with a “flagged” outcome
    • no ratings are updated
    • no coins are won/lost
  • players flagged too many times are quarantined
    • a quarantined player will only automatch with other quarantined players,
      keeping the trolls away from the legit players
  • since matchmaking is between random players, collusion is unlikely
    • one user controlling two clients cannot “throw” or lose on purpose to boost
      the rating of the opponent as they won’t likely be automatched (given
      a big enough playerbase)
    • matchmaking via direct challenges do not update ratings or transfer coins

Betting 💰

  • betting or wagering is virtual; no real life currency
  • the unit of virtual currency is the “coin”
  • users are given a “signup bonus” on signup (e.g. 1000 coins)
  • to play a match, both players agree on a bet amount
  • we check up front that the players have enough coins to cover bets
  • since we don’t support more than one match per user at any time, it is fine to
    not take/debit coins up front.
    • the idea here is that we likely have mobile clients that will disappear and
      not return to finish a match. Thus, we timeout matches after some time.
    • not taking coins unless a match finishes normally means that we don’t have
      a lot of tricky match state to monitor in order to issue refunds
  • A debit only occurs when a match ends normally
    • we take bet coins from the losing player’s account.
  • A credit occurs when…
    • a match ends normally; we pay the winner; and
    • a user purchases a coin product via in-app purchase and the transaction is
      verified on the server.
  • There is no “rake” or “house charge”.
    • The winner of a normal-outcome match earns the coins of the loser equal to the match’s bet.

Timeouts ⏰

  • pending automatches that fail to find an opponent are cancelled (e.g. 2
    minutes later)
  • active matches that fail to end are cancelled (e.g. 12 hours later)
  • ended matches are removed from the database a while later (e.g. 12 hours
    later)

Tech

  • Node.js
  • Redis
  • Socket.io

Scalability 🤗

  • Scale out server nodes; all talk to the same Redis instance
  • Scale Redis server up as needed
    • You won’t need more than 100 MB using this default server

Socket.io Messages

Client to Server 📥

  1. once('signup', ())
  2. once('checkin', (token))
  3. on('automatch', (rules, bet))
  4. on('match event', (eventType, eventData))
  5. on('vote for winner', (winnerId))
  6. on('flag opponent', (reason))
  7. on('get products', ())
  8. on('verify purchase', (productId, receipt))
  9. on('change display name', (newName))

Server to Client 📤

  1. emit('name', userDisplayName)
  2. emit('coins', delta, balance, reason)
  3. emit('token', token)
  4. emit('error', context, message)
  5. emit('match', $match)
  6. $match = {
  7. id: string,
  8. rules: string,
  9. bet: int,
  10. status: string,
  11. outcome: string?,
  12. flagReason: string?,
  13. p1: string,
  14. vote1: string,
  15. name1: string,
  16. stats1: string,
  17. p2: string?,
  18. vote2: string?,
  19. name2: string?,
  20. stats2: string?,
  21. winnerId: string?,
  22. created: string,
  23. started: string?,
  24. ended: string?
  25. }
  26. emit('products', [$product])
  27. $product = {
  28. id: string,
  29. coins: int
  30. }
  31. emit('match event', senderId, eventType, eventData)
  32. emit('presence', userId, isPresent)
  33. emit('match started', $match)
  34. emit('match ended', $match)
  35. emit('match started', matchId)
  36. emit('match stats', [$stats])
  37. emit('server metadata', $serverMetadata)
  38. $serverMetadata = {
  39. usersOnline: int
  40. }

Redis Schema 🗄

Users 👥

  1. u/$userId => hash
  2. name => string Generated or user-defined non-unique display name
  3. coins => int Current balance of virtual coins
  4. matchId => string? Current (only one!) match id if any
  • User data does not ever expire

Pending Matches ⏱

  1. pm/$rules/$bet => set of $matchId
  • Members added when automatch fails to join a match and thus creates one
  • Members removed when corresponding match isn’t found (expired) or is joined

Pending Matches for Quarantined Users 😷

  1. q/pm/$rules/$bet => set of $matchId
  • Members added when automatch fails to join a match and thus creates one
  • Members removed when corresponding match isn’t found (expired) or is joined

All Matches

  1. m/$matchId => hash
  2. id => string e.g. "01BY10N9HQ01EFGZARHE0MK4YF"
  3. rules => string rules of play
  4. bet => int > 0
  5. p1 => string $userId
  6. name1 => string cached $p1.name
  7. vote1 => string p1 | p2
  8. p2 => string? $user != p1
  9. name2 => string? cached $p2.name
  10. vote2 => string? $p1 | $p2
  11. status => string "pending" | "active" | "ended"
  12. outcome => string? "normal" | "conflict" | "flagged"
  13. created => string ISO-8601; e.g. "2017-11-03T13:21:59.788Z"
  14. started => string? set when status "pending" => "active"
  15. ended => string? set when status "active" => "ended"
  16. winnerId => string? nil | $p1 | $p2

To be explicit, expiring a match means removing it from Redis

  • Pending matches expire 2 minutes after being created
  • Active matches expire 12 hours after becoming active
  • Ended matches expire 12 hours after ending

User Stats 📊

  1. us/$userId/$rules => hash
  2. elo => int current Elo rating (default 1200; range ~ 0-5000)
  3. played => int matches played (all-time)
  4. won => int matches won (all-time)
  5. winnings => int coins earned from winning matches (all-time)
  • User stats data does not ever expire

Metadata

  1. metadata => hash
  2. sockets => int Number of currently connected users (CCU)

Configuration 🎛

Some elements of Dueling Monkeys are configurable via environment variables:

  1. PORT=3000 Socket.io port
  2. REDIS_URL=redis://localhost:6379 Redis instance to use
  3. SECRET=foobar JSON Web Token signing key
  4. SIGNUP_BONUS=1000 Coins to give new user on signup
  5. PENDING_TIMEOUT=120 Cancel pending automatches after seconds
  6. ACTIVE_TIMEOUT=43200 Cancel active matches after seconds
  7. ENDED_TIMEOUT=43200 Delete ended matches from Redis after seconds
  8. FLAGGED_LIMIT=20 A player flagged this many times gets quarantined
  9. CLEAN_NAMES=1 Replace profanity in usernames with '*' if 1
  10. MIN_NAME=3 Min length of user display names
  11. MAX_NAME=32 Max length of user display names
  12. REDIS_PRODUCTS_KEY=products Where coin products JSON is stored in Redis

Coin Products for In-App Purchases 💰

Coin products for sale are configured in Redis. The idea is that you can update
which products are available without a new server code deployment. The format
of the value is JSON of the form [{"id":string, "coins":int}]. These are made
available to all clients regardless of platform.

Applicability

Think Chess, Space Invaders, “.io” games… not Quake 3 😬

License

MIT

Copyright

Copyright (C) 2017 Fictorial LLC.