Skip to content
/ whist Public

Whist is a trick-taking, real-time multiplayer card game.

License

Notifications You must be signed in to change notification settings

tsanikgr/whist

Repository files navigation

Table of Contents

Whist

Whist is a trick-taking card game with many variations. The one implemented here is very similar to Oh Hell. You can download the app from the Google Play Store. Please note that the code & documentation presented here are not up to date, as many more features have been added.

Features

  • Single player against computer opponents
  • (Android only) Real-time multiplayer using Google Play Games Services (invite friends and/or play with up to 3 random opponents)
  • (Android only) Achievements and Leaderboards
  • Statistics, game settings & variants

Planned future work: tests, iOS app, app invites & deep links, more battery efficient computer opponents, better graphics, in app purchases, and more.


I decided to open-source my code in order to showcase my work and get valuable feedback. Below, I present a high-level overview of the app --- feel free to contact me or dig in the code for more details. My CV can be found here.

Note: Everything in here is 100% my own work, except where specifically indicated below. This includes the UI design and all graphic assets. I am not very proud of my design skills, so please be lenient with the visual aspect of the game.

Setup instructions

In case you want to build and run the project yourself, you need to:

  1. Follow the instructions from here, to import an existing LibGDX, gradle based project.
  2. Implement the function getStoragePassword() in Config.java, for example by simply returning a string.
  3. Create your own implementation of card shuffling in Dealer.java, for example by calling cards.shuffle().
  4. Setup Google Games Services as explained here.
  5. Modify the ids.xml file, with the application ID and achievement/leaderboards IDs you obtained in step (4).
  6. Modify the signing configurations in the android gradle.build file, with your own keyAlias, keyPassword, storeFile and storePassword (both for the debug and release configurations).

High level overview

The app is built on top of the LibGDX game development framework and it is written in Java. Currently, it compiles for Android and Desktop, and iOS. Platform specific code is placed in the android, desktop and ios directories, whereas all platform independent code is placed in the core package. It also uses a customised version (to fit my own needs) of the StageBuilder library, a project for building LibGDX stages (screens) from xml files. Please have a look at my other repository, where I supply an Adobe Illustrator javascript which automatically exports graphical assets and .xml files for use with StageBuilder. Once the Illustrator file is updated, the UI of the app can be updated using two clicks, with no code modification needed whatsoever.

One of the primary goals when I decided to write this game from scratch, was to create my own library, tools and workflows. This would allow me to quickly build new games in the future. As such, my top priorities were code re-usability, maintainability, testability and minimal dependencies.

Code Architecture

The structure follows the Model View Presenter (MVP) architecture. That said, please do not be confused: all Presenters extend the Controller abstract class, and follow the XxxController naming convention (after the Model View Controller (MVC) architecture which is very similar).

In the following UML class diagrams, many classes are omitted for brevity reasons, including all XxxModel classes (many are shown as fields). Moreover, only a selection of the members of each class is shown to save space.


Tip: You might prefer to navigate the diagrams whilst reading the descriptions below them.


Controllers

Diagram 1 - Controllers (Presenters)

All controllers inherit from the Controller abstract class, which allows the communication between them.

The diagram is color coded as follows:

1. App entry point (green)

The AppController class is the app entry point. It implements the IApplication interface, which defines methods for the app lifecycle events (e.g. onCreate(), render(), onDispose() etc.).

In addition, AppController is a CompositeController, and is the root of the controllers object graph. In other words, all other controllers are its children or grand-children.

Note: The proposed structure scheme makes it easy to write unit tests. Each controller can be easily swapped for a stub, allowing the independent testing of each one of them. At the same time, communication between them is easy without dependency injections.

2. First level controllers (blue)

The name of each controller (Assets, Storage, CardController etc.) and the members of the interfaces they implement pretty much sum up their responsibilities.

The most interesting one is the composite controller ScreenDirector. It creates the root of all View objects: this is represented by LibGDX's Stage object. Moreover, it creates three ScreenControllers: the LoadingController, the MenuController and the GameController, and activates the appropriate one according to the state of the app.

3. Screen controllers (red)

These are the presenters which handle the creation and the management of their Views.

The GameController delegates game actions to the concrete implementations of the IWhistGameController interface.

4. Game controllers (yellow)

All concrete implementations the the abstract class WhistGameController make up the Whist-specific game controllers:

Note: The GameSimulator, WhistExecutorService, and ThreadedGameModelPool are not controller objects. They are just there to show how the PlayerController implements the bots.

Views

Diagram 2 - Views

All UI elements derive from LibGDX's scene2d Actor class, a graph node that knows how to draw itself on the screen.

LibGDX's scene2d "is a 2D scene graph for building applications and UIs using a hierarchy of actors"

Views are composite actors (they extend Group). They can be built synchronously or asynchronously from xml layout files using StageBuilder. A view is only built synchronously only when it is required immediately. Otherwise, as with every other computationally expensive task, most of the work is performed on a background thread. Execution returns to the UI thread using the command software design pattern whenever required (e.g. for openGL texture binding calls).

Screens

Screens are composite Views. They group UI elements expected to be shown together --- their names are self-explanatory: MenuScreen and GameScreen.

In addition:

  • they are View factories: Given the view's .xml filenames, they instantiate the appropriate sub-classes of View.
  • they manage the lifecycle of all the Views they construct
  • they are Facades to the UI for the IScreenControllers. Most of the communication between views and presenters goes through them.

In the UML class diagram, Views created and managed by the MenuScreen are shown in purple. Those managed by the GameScreen are shown in red.

Models

Models expose methods to update and query their internal states, having no business logic (apart from some input validation when updating). They can be serialised to store them or transmit them through network calls. Most of them are placed in the models package.

AI computer opponents

The game wouldn't be complete without a single player mode against computer opponents. I needed a quick hack to implement this functionality, but simple heuristic rules would make the game boring.

"Although the rules are extremely simple, there is enormous scope for scientific play." [Wikipedia]

Thus, I implemented a simulation-based algorithm, that allows the computer to play the game with no training or prior input.

Although this algorithm is not suitable for a mobile application (... I guess users expect card games to use less battery!), makes the game fun because it is hard to beat. I challenge you to win the bots!

It is multithreaded

Uses an ExecutorService to create a fixedThreadPool (see WhistExecutorService). Synchronisation is achieved using ReentrantReadWriteLocks.

... and recursive

The game is simulated recursively. Whenever a recursive call is made, a read lock is obtained to see whether there are any idling threads. If so, the caller tries to obtain a write lock on the number of running threads: if successful, the number of active threads is incremented, and the simulation continues as a new task on the new thread. Otherwise the current thread continues normally.

... and uses Pools to reduce the frequency of garbage collections

Every time a game action is simulated, a new SimGameModel is created. Instead of creating a new object every time, SimGameModels are recycled whenever they are no longer required. To make matters simpler, one pool is used per thread. See ThreadedGameModelPool.

Note: Due to the nature of the game, the number of SimGameModels space quickly explodes, making it impossible to simulate all possible outcomes. To tackle this problem, some heuristic rules are used to limit the number of simulations per card, when dealing more than 6 to each player.

Multiplayer

The Google Play Games Services API was used to implement real-time multiplayer functionality across devices and users. All of the API specific code can be found in the google package. It's a high level API, so it was very easy to implement the following:

  • Inviting friends to play against them, or playing against random opponents (or a mix of the two)
  • Handling invitations and notifying the user
  • Handling room life-cycle events
  • Creating and matching opponents for different game variants (such as the bet amount)

Interesting bits of my own code can be found in the MultiplayerMessage class (e.g. using a Pool to recycle messages and multiplexing information into single bytes to conserve network usage) and in the Messenger class, whereby an inbox and an outbox are used to properly handle Messages received in the wrong order, or requesting messages that have been dropped to be re-sent. The BaseGameUtils, GameHelper and GameHelperUtils classes where obtained from Google's samples, and where slightly modified to the needs of the game.

Software design pattern examples

One example for each of the following software design patterns is given below.

  • Behavioral

    • Observer

      The IStatistics controller is an observable which notifies the attached observers (e.g the UserView, StatisticsView and CoinsView) when the StatisticsModel changes.

    • Command

      Whenever a task finishes on a background thread, this pattern is used to return to the main UI thread. Concrete commands are encapsulated in Runnable objects and are submitted for execution using the Gdx.app.postRunnable() utility function.

    • Mediator

      Classes implementing the IScreenController interface are concrete mediators: they handle the interaction between UI elements and their corresponding model representations. In other words, ScreenControllers update the Screens, and are informed by the Screens about user events to update the models (Screens inherit from EventListener).

    • Memento

      This pattern is used for game saving & loading. The GameController (originator) supplies the IWhistGameControllers (caretakers) a GameModel object to continue from a previously saved game.

    • Strategy

      Classes implementing the WhistAiInterface can be swapped to create different bots. Classes extending AbstractWhistAi execute the strategy's logic asynchronously by default.

  • Structural

    • Composite

      The object graph of all [Controller]s is formed using the composite pattern. CompositeControllers, such as the app entry point (AppController), delegate work to their child controllers. Also, Screens are composite Views.

    • Facade

      Screens are facades to Views. Most of the communication between [IScreenController]s and UI elements go through them (in other words screens delegate updates to the appropriate View.)

    • Pools

      Pools are used to recycle objects, and hence reduce the frequency of garbage collections. They are used in many places: network messages (MultiplayerMessage), simulated game states (SimGameModel), Actions attached to actors etc.

  • Creational

    • Factory method

      The abstract class [Screen] provides the methods buildViewSync(String), buildViewAsync(String) and getView(String, Class<T>). The subclasses of Screen decide which views to instantiate.

    • Builder

      A variation of the builder pattern is used for the circular reveal animations. [Animators] provide an [AnimatorParams] object, which can be modified to customise the animation. However, [AnimatorParams] objects are not builders per se, since they are members of Animators instead of handling their creation. Nevertheless, they separate the representation of Animators from their creation.

    • Prototype

      GameModels provide a copy(GameModel) member, which returns a clone ...GameModel.

Code metrics

In case you are interested, here are some of the metrics I obtain using the static code analysis tool SonarCube.

  • Total lines of code: 22k
  • Classes: 234
  • SQALE Rating: A
  • Technical Debt Ratio: 1.9% (Note that I am using the default SonarCube quality profile which includes in this metric a lot of minor issues (e.g. replacing tabs with white-spaces).)
  • Directory tangle index: 0%
  • Cycles: 0
  • Dependencies to cut: 0
  • Complexity: 4036
  • Average complexities
    • per function: 2.2
    • per class: 17.2
    • per file: 22.5

App statistics

The android app uses Twitter's Farbic analytic tools to track various performance metrics. The following stats are a combination of the info provided by Google's Developer console and Fabric. (Updated 1st of December 2016, approximately a year after the first public release)

  • ~500 Daily active users
  • 1500-2000 Games per day
  • 15-20 minutes/day time in app per user
  • 110 downloads per day on average (without any advertising, all organic aquisitions)
  • 48% of Play store visitors are converted to downloads
  • 99.4% crash-free sessions
  • 4.34* rating (almost all of the negative reviews complain that the bots are impossible to win!)

Used libraries & code

As already mentioned, I am using a modified version of the StageBuilder library. The relevant code is in the assets and stage_builder packages. In addition, the Base64 class in the Cryptography.java file was obtained from here. The BaseGameUtils, GameHelper and GameHelperUtils classes where obtained from Google's samples. The LRUCache was obtained from here.

About

Whist is a trick-taking, real-time multiplayer card game.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages