For those that partake in sim racing, you likely have access to some auxiliary features that add context to your performance and progress within the game. That often includes record times and ghost laps. That said, what about when friends come over to play? This brings multiple challenges:
- You don't want to overwrite your previous lap records to indicate the performance of a pool of friends.
- In the case of more advanced monitoring of skills (such as ratings in Assetto Corsa Competizione), you don't want to spoil your records.
- Though one could switch off configurable features that would adversely record such data, this takes time and can be disruptive when entertainment is of the essence.
- You wish to record your friends' performance explicitly for future reference.
- You want to share your racing records, and that of your friends, with ease.
- You want a single platform to store racing records between multiple games.
Introductions
Introducing Stopclutch. It's a Django site to manage race times by you and your friends, colleagues, family, even the Queen:
I'm a long-time fan of Django; it's undoubtedly my favourite framework/platform to develop in for many reasons, but more on that later. This was the most prominent software project as of its inception, and one that I'm particularly proud of. Rather than being a pet project born out of curiosity and experimentation, this was born of a real need by my friends, all of whom came over to use my Logitech G27 at one point or another:
The Objective in a Nutshell
To conveniently record, compare, and collaborate the statistics and experience of sim racing between friends (or anyone else).
Models
The model list consists of the following:
Game
Player
RaceTime
Track
VehicleMake
VehicleModel
Game
This model represents a Game
that a racer would play, such as Assetto Corsa or Project CARS 2 (yes, I know, the original is supposedly better). Supporting more than one Game
allows for specificity about RaceTime
instances, and to compare similar setups between multiple Game
instances if so desired:
Ordinarily, images would appear here to adorn vehicle models, but sadly a bug prevented that from working in the staging environment with which these screenshots were made.
Notable here is the cross-section of information given a particular object; this pattern will become more evident as more models are revealed below.
Every model in Stopclutch can be managed through the renowned admin site:
To access this login site and any other non-"site" page, one needs to log in through the standard Django page:
This includes managing any Game
:
Player
Every RaceTime
is registered against a Player
. Conventionally, a Player
would have been one-to-one with a User
, following Django's documentation on the matter. However, the main use case of Stopclutch is for friends to come over and race in my company; this allows me to log in with my credentials, allowing as few users registered on the site as possible. This reduces the site's attack surface area and upkeep effort.
Viewing a Player
shows their RaceTime
collection at a glance. Such a collection is accompanied by colour-coding for quick interpretation of their performance:
RaceTime
Given an instance of a recorded time, its details screen will show the properties of that RaceTime
, as well as other times for the same category:
A category isn't an explicit model, but instead represents a unique combination of Track
, VehicleModel
, and Game
. This is represented by the following model method:
def get_category_times(self):
return RaceTime.objects.filter(track=self.track, vehicle_model=self.vehicle_model,
game=self.game).order_by('race_duration')
As per the screenshot of the Game
admin above, admin screens exist for managing any given RaceTime
:
The same goes for editing a single RaceTime
, bringing together all involved fields:
Track
When viewing a Track
, the concept of a category is made more explicit. This will show the best combinations using all aforementioned model fields excluding the Track
(obviously). A well-filled database will likely show lots of "1st" results here, so there may be some room for improvement which would become evident as the app is battle-tested further:
VehicleMake
Makes are stored independently of models. While these could be combined for simplicity, they are combined here to allow for logos to be stored against them such that they display nicely on-screen, and in case additional fields are to be stored against them going forward.
VehicleModel
A model is stored such that race times can accumulate across combinations of Player
, Game
, and Track
. This once again exercises the concept of a category:
These can be modified within the admin, accommodating optional photos of the model:
API
This site uses Django REST framework to expose an API for select operations:
urlpatterns = [
path('players/', views.PlayerList.as_view()),
path('assetto_times/', views.AssettoTimeCreate.as_view())
]
The idea behind this (which had a successful implementation in WPF at some point) was that the Assetto Corsa "race result file" would be watched and automatically processed, sending the results to this API. The application would load the list of players before running, allowing one to select who's racing before starting a race; the results would then be submitted for the selected player when Assetto Corsa updates race_out.json
at the end of a race.
CI and Deployment
Stopclutch is stored on GitHub. It's built, tested, and linted through GitHub Actions:
It's then deployed and served by Heroku:
I was in the process of converting this project to run on Docker, which would enable it to easily use Redis for caching (and so on).
However, having attained a great amount of experience with Heroku since this site's inception, I realised I was spending (and dare I say wasting) a disproportionate amount of time on the finicky parts of build and deployment.
I ultimately found this to be detracting from the enjoyment of building any application, regardless of the benefits of doing so.
Criticism and Improvements
This was definitely more about learning than mastering. So much about Django and best practice became absurdly clear while implementing Stopclutch.
Part of this learning process involved apps and organising artifacts.
Choices Around Views
Here, I chose to use one app and split the views into multiple files in one module:
One file was then created for each view (or group of views):
Lessons Learned From Views
In retrospect, the way that I felt views needed to be split into individual files demonstrated that this might have warranted app splitting instead of lumping everything into multiple files within one app, or perhaps one massive file.
That said, I haven't had the best experiences around figuring out what to do when apps use each other in Django. Understanding how apps should communicate between each other when best practice indicates that they should be self-sufficient still has me puzzled.
Choices Around Managers
I found myself polluting the VehicleModel
space with methods that I felt didn't belong there, namely finding a random object in the collection. As shown below, I instead found that Django has a specific construct to work around this, indicating that other developers have found this pattern to be common enough:
class VehicleModelManager(models.Manager):
def random(self):
if not self.count():
return None
count = self.aggregate(count=Count('id'))['count']
random_index = randint(0, count - 1)
return self.all()[random_index]
Lessons Learned from Managers
Having "stumbled" upon this Django functionality indicated that it's worth reading through the Django documentation thoroughly.
Documentation can be a bore, but I thoroughly congratulate all contributors of the Django project for their wonderful documentation. It probably makes for light (or even fun) reading for some, and will undoubtedly have a positive return on investment for all Django developers.
Additional Bits
I wrote a short FAQ for anyone who stumbled upon the site from the wild west of Google:
Theming was done using django-bootstrap4, using SASS (SCSS) for a more powerful and customisable method of styling:
The repository is closed-source, since I haven't devoted time to ensuring that the repository history contains no accidental secrets, and so on.
Conclusion
This was a project born out of love of racing and cars, and directly served the needs of myself and my friends. It taught me a lot, and is quite possibly the only personal/pet project that will continue to have a sustained life going forward.
I do hope that this taught you something you didn't previously know, or motivates you to start a similar project of your own. I found it very fulfilling, and am excited at the prospect of improving it going forward and integrating it with services such as Sentry for error monitoring, and Codecov for test coverage.
Until next time, all the best!
Top comments (0)