Basketball GM is a single-player basketball management simulation game. Set your roster, make trades, draft prospects, manage your finances, and try to build a dynasty. Play it in your browser now, completely 100% free!

Player ratings and development beta!

February 7, 2018 - , , , , (1 Comment)

Today I released a new beta of Basketball GM featuring big changes to player ratings.

There were a lot of things wrong with player ratings previously. For example, the career arcs of ratings were very unrealistic. For example, it was not uncommon for a player to enter the league with no jump shot and grow to become one of the all time great shooters, or for a player to enter the league with horrible athleticism but grow to become an elite athlete. Sure improvement is possible – but not that much! I mostly implemented the changes described here to make individual rating changes more realistic.

Read more…

Most Improved Player award

December 4, 2017 - , , , , (0 Comments)

Basketball GM has long had awards every season – MVP, Rookie of the Year, etc. But Most Improved Player (MIP) has been missing for a while. That’s because MIP is harder to compute than other awards. You don’t need just this year’s stats, you need prior years too. And you also need to understand context – is a player actually improving, or just recovered from an injury? Or maybe he’s an established star coming off a bad season? Or maybe his numbers went up, but only because he got more playing time without really improving? It’s complicated.

Read more…

Even more advanced stats!

October 6, 2017 - , , , (0 Comments)

Earlier this week I added a bunch of advanced stats to Basketball GM. Well, now there’s even more.

Team Advanced Stats: Go to Team Stats and switch from Team to Advanced to view stats like Pythagorean wins and losses and offensive and defensive ratings. has a good glossary of terms if you don’t know what some of the stats are.

Player Win Shares and Ratings: Player pages and the Player Stats page now show offensive and defensive ratings along with offensive win shares, defensive win shares, win shares, and win shares per 48 minutes.

Offensive rating is a metric of offensive performance, although you should probably also look at usage rate when evaluating it, otherwise you might think a low usage player like DeAndre Jordan is an offensive star. Defensive rating is a metric of defensive performance, although it has trouble assigning credit to individual players so it is inaccurate for good defenders on bad defensive teams and bad defenders on good defensive teams.

Win shares (WS) is a holistic stat that aims to condense a player’s performance into one number, similar to PER. OWS is the offensive part, DWS is the defensive part, and WS/48 is win shares scaled per 48 minutes played, and a WS/48 of 0.100 is average.

However it is generally better than PER because PER is really stupid in some scenarios, such as high volume inefficient scorers. Check out this guy:

He’s an inefficient volume scorer. PER thinks he’s a star, but WS thinks he’s just a little above average. WS seems more correct to me.

This presents an interesting opportunity for Basketball GM players! AI logic is heavily based on PER, but WS is often better than PER. Until I improve the AI, you can easily get a leg up by looking at players who are rated differently by PER and WS.

WS is not perfect though. The main problem is the same thing I mentioned about defensive rating: it will underrate good defenders on bad defensive teams, and it will overrate bad defenders on good defensive teams. So be careful or you might wind up signing the next Enes Kanter to a max contract!

You guys like stats, right?

October 2, 2017 - , , , (0 Comments)

I rolled out a few new features over the past week, all aimed at one goal: more stats. Ideally, Basketball GM should provide you with all the advanced stats available for real basketball leagues. It’s not there yet, but it’s closer. Here’s what’s new:

Team Opponent Stats: On the Team Stats page, you can now switch between team stats, opponent stats, and advanced stats. For seasons played before this update, the only opponent stats will be blocks and points, since those have both been recorded for a long time. Then in new seasons you play, you will get all opponent stats.

Team Playoff Stats: Team playoff stats were always recorded, but there was no easy way to view them. Now you can toggle between Playoffs and Regular Season from the Team Stats page, just like you’ve always been able to do for Player Stats. It’s crazy this feature was missing for so long!

Player Advanced Stats: This is the real juicy part, more advanced stats! When I put PER in the game, I never intended for it to be the only advanced stat. I always wanted more, but performance was kind of shitty in general and I didn’t want to add more stuff to slow it down. But now, with the glorious success of Project Iverson, I can add new features without too much concern for performance (don’t worry, I benchmarked it and all these additional stats have very little performance impact).

Player advanced stats now sit in their own table, which you can view on player pages for individual players, or on the Player Stats page by switching “Per Game” to “Advanced”. In addition to the old standbys, I added TS%, 3PAr, FTr, ORB%, DRB%, TRB%, AST%, STL%, BLK%, TOV%, and USG%. See for definitions, cause that’s where I got them from. Most of these won’t be available for past seasons because they are based on the team opponent stats described above, but some will be.

All the % stats (like ORB%) are calculated using the formulas at the page linked above, which means they are estimates. I could have stored data directly from game simulation to calculate real values, but I didn’t do that. No real reason why. Arguments could be made either way, but at the end of the day I doubt it matters much.

Hopefully this is just the start of more advanced stats. I’d love to add a better holistic stat than PER, which is really not a good metric. What do you want to see next? Let me know on Reddit.

More extreme heights

September 9, 2017 - , (0 Comments)

One of the cool things about Basketball GM is the time scale. The NBA has been around for 70 years, and in those 70 years all kinds of crazy things have happened – freakish players, lucky shots, huge upsets, tragic deaths, and more. In Basketball GM, you can easily play 700 years – you should get 10x as much craziness as the NBA! And you do get some.

But one thing that is missing is extremely freakish players. The best players alway feel kind of the same. That’s because of the 0-100 rating system – once somebody is near 100 in most categories, he’s the same as somebody else who is near 100 in most categories. You could imagine fixing that by abolishing the 0-100 system and letting ratings increase unbounded, but that is a bit too radical for my tastes. A more conservative solution is to decrease the range of normal. Take a 100 rating and make it a 75, then allow anything above 75 to appear only very rarely. That would allow for more unique stars that you might only see after playing thousands of seasons.

Don’t get too excited. I haven’t completely done this yet. But thanks to some help from Timothy Highley, it’s done for height. Short players will no longer have height 0 unless they are really short, and tall players will no longer have height 100 unless they are approaching physiological limits.

This is an important change for people making custom rosters or running multiplayer leagues. When loading a league file in the new version, player height ratings will be adjusted to reflect the new scale. If you don’t want this to happen (such as, if you’re building a new roster file and want to specify the heights on the new scale yourself), add this to the root of your JSON object:

    "version": 24,

In the future, if there are more backwards incompatibly changes in file format, the version parameter will be used similarly in upgrades. For now, it only affects heights.

If you have any feedback, please leave a comment on Reddit.

Live draft lottery

July 8, 2017 - , , (0 Comments)

You can now view the draft lottery live, as it happens! The lottery behaves the same as it always has, just like the NBA draft lottery, so this doesn’t change gameplay at all. But it can be very dramatic to watch the lottery unfold before your eyes.

Making a game 10x faster changes how people play it

April 7, 2017 - , , (0 Comments)

Basketball GM 4.0 was released a week ago. It made game simulation about 10x faster. After releasing it, I was very curious how players would respond. If they played the same amount of time, they could simulate 10 times more seasons. Or they could play 1/10 of the time, but simulate the same amount of seasons. Or something in between. Or maybe they’d even change how they play, like focusing more or less on the details of the game.

Let’s look at some numbers.

Last Week This Week % Change
Users 7,567 8,542 12.88%
Sessions 28,595 31,488 10.12%
Pageviews 2,015,921 2,834,360 40.60%
Pages / Session 70.5 90.01 27.67%
Avg Session Duration (minutes) 30.25 27.5 -9.09%
New Leagues 10,172 15,360 51.00%
Completed Seasons 37,145 70,436 89.62%

The biggest change is that nearly twice as many seasons were played. And since the number of new leagues went up only 51%, it suggests that people are playing more seasons per league, which makes sense.

The other interesting thing is that while pageviews per session are up 28%, suggesting that users are doing more stuff per session, the actual time played per session is down by 3 minutes. So more is being done in less time.

How has version 4.0 changed how you play? Let me know on Reddit.

Basketball GM 4.0 technical details – caching, Shared Workers, IndexedDB/Promise interactions, Safari being a tease, McDonald’s, and more

April 2, 2017 - , , , , , (0 Comments)

Google made me do it.

Basketball GM has always allowed you to open up the same league in multiple tabs, so you can easily view multiple different screens. This was originally implemented by running the entire game in each tab. Game data was always saved to disk via IndexedDB. And when an action resulted in a change to the data (such as playing a game, signing a contract, trading a player, etc), then a signal was sent to all other tabs telling them to update their data. This was kind of a crude approach, but it worked.

It worked, until Chrome started throttling JavaScript in background tabs. Their logic was, if you’re not even looking at the page, do you really want it burning through your battery? It made perfect sense. Except for Basketball GM, it meant that game simulation would only run if you were looking at the tab you started it in. So if you clicked “Play until playoffs” and then switched tabs, it’d never reach the playoffs. Fuck!

When life hands you lemons, make lemonade. Since Google decided to totally invalidate the tradeoffs I had considered when designing Basketball GM, I decided to re-evaluate. I came up with two ideas:

  1. If the current method of multi-tab play doesn’t work, then I don’t need to write everything to disk all the time. The only reason I was doing that was for cross-tab communication. Instead, I can keep a cache in memory, and then it should all be faster!
  2. Then, to restore multi-tab play, I can try to use some cross-tab communication technologies that didn’t exist when I first made Basketball GM, specifically Shared Workers and Service Workers.

These would both be major changes to the core of the game code. I had considered doing both previously, but never made a serious attempt because it was so much work. But now, with Google fucking up multi-tab play, I really needed to do something. So I began “Project Iverson” (get it, speed?) and set off with great optimism working on the first task, replacing most database calls with a cache.

The basic idea of my cache is that all data needed for “normal” gameplay should be stored in memory. Database access should only be needed for viewing historical data. For example, all non-retired players are stored in memory, but retired players are not. So game simulation can happen without database access, and most pages can be displayed without database access. Only viewing pages that display retired players requires reading data from disk.

It sounds simple, but it’s easier said than done. Some of the challenges included:

  1. If I read some data from the cache, that data is mutable, meaning that any changes I make to it will be immediately reflected in the cache. Good for performance, but bad if I have some code that is accidentally altering my data, which is possible because data read from IndexedDB can be safely mutated without affecting the database, so the entire game was not written with this mutability constraint.
  2. Previously I relied a lot on indexes to retrieve data. Like “get all players on team X” or “get me team stats from 2052”, basically this is a built-in database feature to efficiently retrieve a subset of data. I needed to reimplement this in my cache. Even trickier, I needed to keep the indexes up to date even when data changes, with good performance. For example, if a player is traded from team X to team Y, then I need to make sure that the “get all players on team X” index does not return stale data.
  3. An in-memory cache also needs to eventually be persisted somehow. With IndexedDB, it was easy – every time I updated data, it was automatically saved to disk. I could do the same with my cache, but writing to IndexedDB for every single update would be be way too slow. My solution was to keep track of which objects in my cache are “dirty” (have changed since the last sync to disk) and then have a function that writes all dirty objects to IndexedDB. Then, this function is called periodically in the background to ensure the cache and disk never fall too far out of sync.

Ultimately, I came up with (IMHO) a pretty decent caching module and started using it everywhere. I started with game simulation, the most CPU intensive part of the codebase. Results were promising – a 50% speedup! But as I converted more and more of the UI to my cache, that 50% speedup gradually disappeared. Even worse, the UI became less and less responsive to the point where it actually felt worse than it did originally! Hey, what gives?

I think the problem was that JavaScript is single threaded. So while IndexedDB could do a lot of work off the main thread, my cache could not. Instead, when my cache was doing stuff, it would block the UI from updating, making it appear very slow and horrible.

This was not good. I had just spent a lot of time working on this thing, and it just made things worse! But did I give up? No! On to phase 2, leveraging Workers!

By “Workers” I mean three different things: Web Workers, Shared Workers, and Service Workers. They all allow you to run JavaScript code off the main thread, which could potentially solve the problem with my cache. And they all are a bit different:

  • Web Worker – A page can spawn a Web Worker to run some JS in another process. They can also communicate, sending signals back and forth. When you close the page, the worker goes away. This is supported in nearly every browser.
  • Shared Worker – Like a Web Worker, except it can be shared across multiple instances of a page, like if you open the same page in multiple tabs, as often happens when playing Basketball GM. Unfortunately, it’s only supported in Chrome and Firefox, and Safari and Edge will probably never support it because Apple and Microsoft are dicks. However, Basketball GM already only runs in Chrome and Firefox, so maybe that’s not insurmountable…
  • Service Worker – Like a Shared Worker, except it has a bunch of cool extra features and one big limitation: if you do a computation that lasts longer than 30 seconds (such as… simulating a season in Basketball GM), the browser will kill it. Ouch.

Based on that, I started with a Web Worker, because it’s simple and globally supported. However, Web Workers do not make it easy to support multi-tab play, and that was originally the whole point of all this, right? So my plan was to get it working in a Web Worker, and then add support for Shared Workers in browsers that support it.

This also required a ton of work! Because Basketball GM runs entirely client-side in your browser, it was easy for some bad habits to creep in. There was fairly tight coupling between the UI (code to display pages, handle links, update UI) and the backend (game logic, data manipulation). But I wanted to run the backend in a Worker, which required entirely splitting the UI and backend, and defining an API for them to communicate. I wrote a nifty little library called promise-worker-bi to make communication easier. But there was a serious technical problem, a more troubling incantation of a problem I had dealt with in the past: IndexedDB and Promises don’t play nice.

Let’s do some more technical digression. As mentioned above, my cache still ultimately saved game data to IndexedDB. IndexedDB has a ridiculously horrible and painful API which can only be rescued by wrapping it in Promises, which is the new standard way of handling asynchronous operations (IndexedDB has a crazy amount of asynchronous stuff going on). I wrote a library called backboard which does a decent job of this, and I’ve been using it in Basketball GM for a while.

However, it doesn’t work inside a Worker! Well, it does in Chrome, but not in Firefox! Why? Because Promises in Firefox don’t play nice with IndexedDB. Previously I had hacked around this by using a third party Promise implementation rather than native Promises. However the tricks that these third party Promise implementations use to play nice with IndexedDB do not work inside Workers! Fuck.

I spent a while trying to figure out a nice way around this, but there wasn’t one. Fortunately, the cache meant that I didn’t actually have that much code doing complicated stuff with IndexedDB. So I very carefully went over all of my database code and ensured that it didn’t mix IndexedDB and Promises.

Eventually I got it to run. I decided to do a quick benchmark and see if now, after all this, it actually has decent performance. I was optimistically hoping to recover that 50% speedup I initially saw after implementing the cache. Instead, I saw a 10x increase in performance! Fuck. I was really excited. I made a vague post on /r/BasketballGM entitled HOLY SHIT YOU GUYS HAVE NO IDEA WHAT I JUST ACCOMPLISHED and celebrated my accomplishment by taking my girl out to McDonald’s where I got a well-deserved Grand Mac and she got a less well-deserved Mac Jr.

But wait, how was that possible? 10x performance boosts don’t just fall out of thin air. I think a big part of the answer is that I (like many others) assumed that IndexedDB being asynchronous meant that it would not interfere with the UI, that I could do IndexedDB stuff in the main thread without impeding performance. But I never actually tested that. I did notice that Nolan Lawson showed it was actually not true, but I still wasn’t expecting a 10x performance increase! Moral of the story: if you are doing IndexedDB, put that shit in a Worker! Even if you have to suffer through IndexedDB/Promise incompatibilities. 10x is worth it.

Around this time, there was even more good news. Safari has long had very serious issues with IndexedDB, so Basketball GM never ran in Safari. And because Apple is evil and won’t let anyone else release a browser on iOS (no, Chrome is not real Chrome on iOS, it’s just Safari with a different UI), Basketball GM has never worked on iPhones and iPads. But then Apple released Safari 10.0.3, which was good enough to partially play Basketball GM. It would let you get up to the playoffs, at least. But on my development version, it actually worked really well! Sure it was a bit slower than other browsers, but finally there were no weird errors! So I figured I was going to release a new version of Basketball GM that (1) has fixed Chrome multi-tab support; (2) is way faster; and (3) works in Safari. I released a beta, to much critical acclaim.

But wait, I hadn’t actually fixed multi-tab support. The beta only let you open a league in one tab, which kind of sucked. My plan for that was to move from a Web Worker to a Shared Worker, but that wouldn’t work in Safari because Safari doesn’t support Shared Workers. Crap. I decided to implement Shared Worker support, but keep the Web Worker mode around as a fallback. I put a lot more work into promise-worker-bi to make it seamlessly work with either a Shared Worker or a Web Worker. I rewrote a lot of backend code that made the assumption that it was only talking to one instance of the UI. Humorously, I introduced a new limitation: instead of letting you only open one tab for a league, you could only open one league (but in as many tabs as you want). I figured that is a more palatable constraint, and it would be a lot of work to make the backend support having multiple leagues loaded at the same time.

And all this worked! The Shared Worker did run a bit slower than the Web Worker, for whatever reason. But it was still way faster than before. And multi-tab play was back, even in Chrome! Because the Shared Worker is shared by all tabs, the whole “simulation running in a background tab” thing never happens. It’s always in the active tab!

I released another beta for people to test. By the way, I really appreciate all the testing people did, a lot of bugs were found in the beta. Special shout out to the homie Jerick Don San Juan for finding the most.

While I was waiting for people to test it, some other stuff happened:

Some people on the subreddit asked for table filtering, so I added it. It was actually very easy to add, and it was purely a UI thing so it didn’t interfere with any of the other in-progress work.

I got rid of the jQuery dependency! Basketball GM started out as a horrible mess of spaghetti code because I didn’t know how the hell to write JavaScript (in my defense, basically nobody did back then). So of course I used jQuery everywhere. By now, I had gotten rid of it everywhere except two places: AJAX requests and roster drag-and-drop reordering. Since jQuery doesn’t run in a Worker and some AJAX requests happened in the backend, I replaced it with the new Fetch API. So all that was left was drag-and-drop roster sorting. There is a module called React DnD that in theory could replace jQuery for that purpose, but when I tried it in the past, I found it to be horribly overengineered and complicated. Now, there is an alternative called react-sortable-hoc which is actually pretty awesome. I switched to that, and BAM! no more jQuery! I guess it doesn’t really matter from an end user perspective, other than making the initial load a bit faster. But it feels really nice to me!

And then, Apple. Safari 10.1 was released, which boasted much improved IndexedDB support. I was excited! Are the bugs really all gone? Is it going to be as fast as Chrome and Firefox? Initial reports were that it was good enough to fully run the old version of Basketball GM. But what about the beta? After forcing my girlfriend to update her iPhone, I eagerly waited to see the result. And the result was… fuck, a hard crash of the browser? Are you fucking kidding me? Jesus, Apple, get your shit together!

Well I wasn’t about to hold up the 4.0 release just for Apple, so I said fuck it and released version 4.0 on April 1, an anti April Fool’s Day joke. The real joke is that, after several years of Safari being shitty in various ways, there were actually a couple weeks where it worked either on the beta (Safari 10.0.3) or on the old version of Basketball GM (Safari 10.1). But now it’s back to not working. What else is new? So the race remains – will Apple or Microsoft be first to make a browser good enough to consistently run Basketball GM? Time will tell…

If you somehow made it to the end of this post, go check out Basketball GM on GitHub. Play around with the code. Contributors are always welcome. At least give me a damn star!

Basketball GM 4.0 is here!

April 1, 2017 - , , , , (11 Comments)

This is not an April Fool’s Day joke! Basketball GM 4.0 is really here, and it’s awesome. For those who haven’t been following along, here are the biggest changes:

  • The game runs ridiculously faster than it used to.
  • It’s so fast that it’s actually playable on phones and tablets (iPhone/iPad support is pretty flaky still, but should improve in the near future).
  • You can easily apply complex filters to tables.

I’ll make another post with more technical details soon, for those who are interested in such things.

Thank you to everyone who tested the betas. Hopefully we found all the bugs, but if not, please report bugs on Discord or Reddit.

Basketball GM 4.0 Beta 2

March 24, 2017 - , , , , , (0 Comments)

Thanks you everyone for testing the first beta! All of the bugs people found have been fixed. The biggest change from the first beta is that multi-tab play is back. There are two caveats. First, you can only have one league open at a time. I figure this is not a huge problem, but let me know if you often do have multiple leagues open. Second, it is a little slower than the first beta, but still much faster than it used to be.

Also, multi-tab play does not work in Safari, so you can either play one tab per league in Safari or use another browser.

With that being said, here’s the link to beta 2, please give it a try. To make sure you’re on beta 2 and not the original beta, look at the bottom of the page. It should have a date of 2017.03.24.1059 or later.

Like last time… this is a beta, so bang on it, try to break it, try to get it to produce an error. Feedback is appreciated, on Discord or Reddit.

Older Posts »