Introduction
Best of Steam is a site for finding games to play. The games are essentially sorted by their user review score, but I use a more sophisticated scoring formula than Steam that better aligns with human perception of a "good" score. It is updated every day with new games and up-to-date review scores.
By default, the site just displays the highest-rated games overall, but I recommend using the sidebar to filter the list. Maybe to genres you might like more, maybe to newer releases if you feel you have already seen all the old games Steam has to offer, or maybe something else you prefer.
Games with fewer than 10 positive reviews are not listed.
A game listing
A game listing has the following parts, from left to right:
- Its image
- An icon at the top right of the image indicating whether it's in library, wishlisted, or ignored. More about that under Import / Export.
- Its ranking among the games that pass the current filters.
- Its name
- Its most relevant tags. Most games have more tags than displayed, for the purpose of filtering. Some meta tags have been excluded, such as .
- An 18+ indicator if the game is age-restricted. This is only for games marked as "Adult only" on Steam. A game may still have sexual content without this indicator.
- Its current price in euros. If the game is on sale, this shows the sale price.
- Its release date. This is the date the game claims it came out. It is not when it was actually listed on Steam.
- All platforms it's available for. The icon stands for SteamOS / Linux. A red icon means the game has no non-VR mode.
- The larger percentage is the calculated score. More about that under Scoring formulae.
- The smaller percentage is the percentage of positive reviews to total reviews.
- The review count. This is Steam's default review count and only includes reviews by people who bought the game through Steam.
Filters
Filters provide the main functionality of the site, helping you narrow your search down to the games you actually care about. Several filters are provided by default, but you can create new ones yourself, or use some created by others. More about that under Custom filters. The default filters are:
- Include tag - Shows only games which contain the specified tag. Multiple Include tag filters filter to games that have all the specified tags. Of note is that while Steam shows up to 20 tags for each game, I only use half of those for this filter, because I find this strikes a good balance between including relevant tags and not including irrelevant tags. Sadly, many games, especially smaller games, have incorrect tags or joke tags, but that's just a shortcoming of user-generated content. I can not fix this.
- Exclude tag - Shows only games which do not contain the specified tag. Multiple Exclude tag filters filter to games that have none of the specified tags. Otherwise identical to Include tag.
- Include meta tag - Shows only games which contain the specified meta tag. A meta tag may specify support for an OS, VR, Steam Deck, Controller, various type of multiplayer, or other features Steam lets developers specify. Otherwise identical to Include tag.
- Exclude meta tag - Shows only games which do not contain the specified meta tag. Otherwise identical to Exclude tag.
- Price - Allows you to specify the upper and/or lower price bounds. Leaving either field empty only enforces the other.
- Release date - Allows you to specify the "from" and/or "to" release date bounds. The year field only takes the last two digits of the year, specifying the years 2000-2099. Leaving any field empty in the "from" input defaults that field to the smallest value (1st / January / 1970). Leaving any field empty in the "to" input defaults that field to the largest value (31st / December / 2100).
- New releases - Shows only games released in the past specified number of days.
Presets
Presets allow you to save a combination of filters to quickly switch back to at a later date. Simply add all the filters you want to use, fill them with the desired values, then enter a preset name and click "Add". You can now change or remove the filters however you want, and if you ever click on the saved preset again, it replaces all current filters with the ones you saved earlier. This even works with custom filters.
The default Reset preset removes all filters.
Settings
Settings allow setting the number of games displayed per page (capped at 200 for your safety). They also allow you to set some common filters. You can exclude VR-only games you can't play because maybe you don't have a VR headset, or exclude adult games because maybe that's not what you're on Steam for. After importing your Steam data, you can also hide games you already own, have wishlisted, and/or have ignored on Steam. More about that in the next section.
Import / Export
Best of Steam remembers your browsing sessions and saves all your applied filters, presets, settings, and most everything else on the site. This data should not be lost unless you uninstall your browser, run out of disk space, or remove it manually. If you wish to create a backup of your data or export it for use on another device, then you can do so with the "Export / Backup" button. It can also be used if you wish to share your presets or custom functions with other people.
If you wish to see or filter out games you already own, have wishlisted, or have ignored on Steam, you can import your Steam data. To my knowledge, there is no private information included in the data, and in any case, none of the data will leave your computer. Even Exporting your data will not export your Steam data.
You must be logged into Steam in your browser for the link to contain your data.
Finally, you can import a previously created Best of Steam export. You can separately choose whether or not to import active filters, presets, settings, and custom functions. "Overwrite" will first remove all the data of that category and then add all the data in the imported file. "Append" will replace data with the same keys (preset names, all settings, custom functions with the same names), but otherwise leave your previous data intact, only adding the data in the imported file. "Unused" will not use the data for the category in the imported file. Do not import custom functions from sources you do not trust, as it allows them to run arbitrary code in your browser.
Scoring formulae
While I don't recommend most people change the scoring formula, you may if you are dissatisfied with how the games are sorted. I have done a lot of experimentation with different formulae, and by default, you can switch between the three most promising ones I've found. They are all functions in the form score = f(positiveReviews, totalReviews)
, essentially weighing the overall positive review percentage against the total number of reviews, to varying extents.
To put it simply, neither just a positive review percentage (maybe only 10 of the developer's friends rated the game highly) nor a high review count (games with millions of players may still be poorly rated) is a good indicator of how good a game is. But it is quite obvious that between games with an equal number of reviews, the one with more positive reviews is better. Similarly, between games with the same percentage of positive reviews, the more popular one is better, because it has a broader appeal, meaning you, as a random person, are more likely to like it. Also note that niche games tend to receive a larger percentage of positive reviews because people who would review them negatively don't pick them up in the first place.
These ideas are the basis on which I built the scoring formulae.
- Default
score = positiveReviews / totalReviews * (1 - (totalReviews + 1) ** -(1 / 3))
Basically, we assume a 0% score for a game with no reviews, and for every time the review count increases 8 times, we make the default 0% score half as relevant and the real score that much more relevant. So for 7 reviews, the final score is 50% of the real score. For 63 reviews, it's 75% of the real score. 511 reviews, 87.5% of the real score, and so on.
Compared to the Old Default, this lowers the ranking of most games with fewer reviews, while simultaneously raising the ranking of games with fewer reviews among games that have a lot of reviews. In other words, it makes the curve that determines how the review count affects the final score more steep.
- Old Default
score = positiveReviews / totalReviews - (positiveReviews / totalReviews - 0.5) * (totalReviews + 1) ** -0.25
This is essentially the same as the Default formula, but with the constants changed. We now assume a 50% score for a game with no reviews, and halve the relevancy of that every time the review count increases 16, not 8, times. So for 15 reviews, the final score is
50% * 50% + 50% * real score
. For 255 reviews, it's25% * 50% + 75% * real score
. 4095 reviews,12.5% * 50% + 87.5% * real score
, and so on. - Log. Smoothing
score = (positiveReviews / totalReviews * log10(totalReviews + 1) + 0.5) / (log10(totalReviews + 1) + 1)
This is based on additive smoothing, also known as Laplace smoothing, which works by adding a number of fake positive and negative samples to the existing data. This smooths out variance in the data, especially for cases where there are only a few data points, or reviews in this case. The problem is that for data sets where some items have just a few data points and others have hundreds of thousands of data points, additive smoothing would overly smooth, in our case, games with just a few reviews, and insufficiently smooth games with a lot of reviews.
I have solved this issue by putting review counts on a logarithmic scale.
score - sin(4 * PI * score) / (6 * PI)
If you feel you could do a better job, you can also write your own scoring function which will be used to sort the games. Enter a name for the formula, click "Add", and then click the icon to edit it.
Writing custom functions will be explained in more depth in the next section, but for a scoring function, you are expected to write the body for a function with the following signature: (game: Game) => number
In other words, you're given a Game
object (explained in the next section), and you're expected to return a number. For the purpose of sane formatting in the UI, the returned number should be between 0 and 1.
A working example function is provided for you when first opening up the edit window. If you're curious what it is, it's my take on the "Hidden gems" formula that was popular some time ago.
While I believe that any serious and general scoring formula should only use the positive and total review counts, you have access to all the fields of a Game
object. You can use those to do whatever you want, like sorting by release date, sorting by price, or maybe creating a soft filter that doesn't outright filter games out, but penalizes their score to some degree based on your preferences.
Custom filters
Custom filters are custom functions like custom scoring formulae, but slightly more complicated. I will first go over some general info about writing custom functions.
All custom functions are written in plain JavaScript, but I will be using TypeScript syntax to explain the types of function parameters, return types and class fields. You may have a better experience writing these functions in a dedicated code editor and pasting them back here.
Because a filter may be used multiple times, or not at all, you need to not just define a filter function, but what is essentially a filter function factory that also creates the UI for the filter. For simple, non-configurable filters, this isn't as hard as it sounds. When clicking "Add" to create a new filter, and then editing it, you can see I have already provided code for creating the default UI, as well as a sample filter function.
The following is a list of types that you may need to know. Don't worry about it for now, I will explain them in detail as we go, and this section can be used as a reference.
You are expected to write the body for a FilterCreator<T
, which has the following signature:
(
update: (...oldValues: string[]) => void,
initialValues: string[],
createInput: (
parseFunc: (value: string) => T | null,
callback: (value: T) => void,
parent: HTMLElement,
initialValue?: string
) => HTMLInputElement
) => [ HTMLDivElement, Filter ]
A Filter
is a function (game: Game) => boolean
A Game
is an object with the following properties:
name: string
appID, positiveVotes, votes, score, price, metaTags, tempImageNumber: number
release: Date
owned, wishlisted, ignored: boolean
imageID: string | undefined
tags: number[]
tagNames: string[]
score
is the score returned by the scoring function.metaTags
is a bit field. The meaning of each bit is described under Steam data dump.tagNames
is an array of all existing tags. It is not unique to a game, it can just be accessed here for convenience.tags
is an array which contains the indices of the tags for this game. The indices are for the tagNames
array.imageID
and tempImageNumber
contain additional info to form the URL for the game's banner image.
If you just want to create a non-configurable filter, you do not need most of the above information. Simply open up the editing window and replace the (game: Game) => boolean
function on the last line with whatever you want to filter out. Remember that returning false
for a game filters it out.
However, if you wish to create a configurable filter, such as most of the default ones, then you are going to need to include input fields for the user in your UI. This means defining your own UI for the filter, as well as defining functionality for persisting the filter's state through page reloads. To assist with this, each FilterCreator
function gets some helpful inputs.
Firstly, createInput
is a function that lets you create a validatable input field. You can roll your own inputs, but I will be using the input fields created by createInput
to explain the functioning of a custom filter.
Most importantly, createInput
requires a parseFunc
, which validates the text the user types in. This function is invoked with the string value of the input after every keypress. It should return the parsed value, or null
if the string is invalid. Note that returning null undoes the keypress, so make sure to not return null for any strings which must be typed as an intermediate step to typing a valid string, including the empty string. If you wish to discard such invalid intermediate strings, you can do so in the callback.
The callback
is invoked with the successfully parsed value once the user has committed the change. You may want to call update
inside this callback.parent
is the parent element for conveniently adding the input to the UI.initialValue
is the initially displayed value for the input. It is purely visual and is neither validated nor parsed.
Now, after the user has modified an input on your filter, you may wish to signal to update the game list based on these new inputs. For this, you must call update
.
Additionally, to persist the state of the filter through a reload, you should pass in the string values of all your inputs every time you call update
. These values will be passed back to you in initialValues
every time the page is reloaded. Because of this, you should use these values to initialize the state of your filter, returning it to the state it was when update
was last called. If update
was never called, initialValues
will be an empty array. Conveniently, createInput
's initialValue
accepts the undefined
value you get from an empty array.
I catch any obvious errors from incorrectly defined custom functions, but if you mess up really badly, like writing an infinite loop, you can press F12, navigate under Storage, Indexed DB, SteamDB, find the troublesome function, and delete it.
Steam data dump
If you wish to use the data I have collected on Steam games outside of this site, then you are free to do so. You should make a local copy of the dump, which can be acquired from ./dump.br
. It is updated once a day, before 4 AM UTC. Please do not download it more than once a day, or I may have to restrict access to it.
It is compressed with Brotli and is in a binary format after decompression, comprised mostly of variable-length integers and length-prefixed (also a variable-length integer) strings. All values in the following description are variable-length integers unless noted otherwise. Dates are represented as the number of days since January 1, 0001.
The first value is the dump format version. It is 4 for the format described here. The next value is the date the dump was created. The next value (tagCount
) is the number of tags. The next tagCount
values are the tags as strings. Their order is important. The next value (gameCount
) is the number of games. The next gameCount
groups of values are the games.
A game is the following values: The appID, the name as a string, the number of positive reviews, the number of negative reviews, the release date, the price in cents, the meta tags as a bit field, optionally the hexadecimal ID in the image URL as binary (as an array of 20 bytes), optionally the image URL alternative asset number, the number of tags, and finally the tags as indices of the previously loaded tag array.
The meta tags bit field has the following flags, starting from the least significant bit: VR supported, VR only, Linux, Mac, Windows, adult only, whether the hexadecimal image ID is present, Steam Deck playable, Steam Deck verified, gamepad preferred, full controller support, Steam input API, remote play together, Steam workshop, split screen co-op, LAN co-op, online co-op, split screen PvP, LAN PvP, online PvP, whether the alternate asset number is present, MMO.