Table of Contents
- Whist
- High level overview
- Code metrics
- [App statistics](#app statistics)
- Used libraries & code
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.
- 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.
In case you want to build and run the project yourself, you need to:
- Follow the instructions from here, to import an existing LibGDX, gradle based project.
- Implement the function
getStoragePassword()
inConfig.java
, for example by simply returning a string. - Create your own implementation of card shuffling in
Dealer.java
, for example by callingcards.shuffle()
. - Setup Google Games Services as explained here.
- Modify the ids.xml file, with the application ID and achievement/leaderboards IDs you obtained in step (4).
- 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).
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.
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.
All controllers inherit from the Controller
abstract class, which allows the communication between them.
The diagram is color coded as follows:
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.
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 ScreenController
s: the LoadingController
, the MenuController
and the GameController
, and activates the appropriate one according to the state of the app.
These are the presenters which handle the creation and the management of their Views.
- The
LoadingController
updates theLoadingView
- The
MenuController
updates theMenuScreen
and gets notified about input events. - The
GameController
updates theGameScreen
and gets notified about input & game events.
The GameController
delegates game actions to the concrete implementations of the IWhistGameController
interface.
All concrete implementations the the abstract class WhistGameController
make up the Whist-specific game controllers:
Note: The
GameSimulator
,WhistExecutorService
, andThreadedGameModelPool
are not controller objects. They are just there to show how thePlayerController
implements the bots.
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"
View
s 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).
Screen
s are composite View
s. 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 ofView
. - they manage the lifecycle of all the
View
s they construct - they are Facades to the UI for the
IScreenController
s. Most of the communication between views and presenters goes through them.
In the UML class diagram, View
s created and managed by the MenuScreen
are shown in purple. Those managed by the GameScreen
are shown in red.
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.
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!
Uses an ExecutorService
to create a fixedThreadPool (see WhistExecutorService
). Synchronisation is achieved using ReentrantReadWriteLock
s.
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.
Every time a game action is simulated, a new SimGameModel
is created. Instead of creating a new object every time, SimGameModel
s 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
SimGameModel
s 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.
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.
One example for each of the following software design patterns is given below.
-
-
Observer
The
IStatistics
controller is an observable which notifies the attached observers (e.g theUserView
,StatisticsView
andCoinsView
) when theStatisticsModel
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 theGdx.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,ScreenController
s update theScreen
s, and are informed by theScreen
s about user events to update the models (Screen
s inherit fromEventListener
). -
Memento
This pattern is used for game saving & loading. The
GameController
(originator) supplies theIWhistGameController
s (caretakers) aGameModel
object to continue from a previously saved game. -
Strategy
Classes implementing the
WhistAiInterface
can be swapped to create different bots. Classes extendingAbstractWhistAi
execute the strategy's logic asynchronously by default.
-
-
-
Composite
The object graph of all [
Controller]
s is formed using the composite pattern.CompositeController
s, such as the app entry point (AppController
), delegate work to their child controllers. Also,Screen
s are compositeView
s. -
Facade
Screen
s are facades toView
s. Most of the communication between [IScreenController]
s and UI elements go through them (in other words screens delegate updates to the appropriateView
.) -
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
),Action
s attached to actors etc.
-
-
-
Factory method
The abstract class
[Screen]
provides the methodsbuildViewSync(String)
,buildViewAsync(String)
andgetView(String, Class<T>)
. The subclasses ofScreen
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 ofAnimator
s instead of handling their creation. Nevertheless, they separate the representation ofAnimator
s from their creation. -
Prototype
GameModel
s provide acopy(GameModel)
member, which returns a clone ...GameModel
.
-
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
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!)
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.