Contents

Introduction A game listing Filters Presets Settings Import / Export Scoring formulae Custom filters Steam data dump

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


In Library
1
0.97€
Portal 2
Platformer Puzzle First-Person
18 Apr, 2011
99.1% 98.7%
332659 reviews

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 Early Access or VR.
  • 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.
Clicking on it takes you to its Steam store page.

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.
To remove an already applied filter, click the trash can next to the "Add filter" button and click on a filter to remove.

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. 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. I have also included here my thoughts on designing these formulae.

While everyone agrees that the review count should be taken into account to prevent cases where a game with just 1 positive review is at the top because it has 100% positive ratings, I believe almost everyone does it wrong.
Steam itself sorts by positive review percentage (score) but also has breakpoints that limit games with few reviews from being ranked higher than highly rated games with a lot of reviews. Not only does this give a bad ranking subjectively, but it also means that sometimes games can end up ranking higher by gaining only negative reviews.
Many other places attempt a more mathematical approach, assuming that the score is otherwise accurate, but has variance due to the low number of reviews. They negatively bias the score by using some established mathematical formula intended for this purpose. This also yields a subjectively bad ranking.
What we actually need to do is understand what the score means, and what we want it to mean. A review score is generally quite accurate, not affected by much variance, but only represents a group of people who have done all of the following things:

  1. Seen the game in the store
  2. Decided they want to buy it
  3. Decided they want to leave a review

While the third criterion is fairly inconsequential, the first two introduce a massive self-selection bias to the score. Steam is more likely to show you a game if you're more likely to want to buy that game. And you're more likely to want to buy a game if you're more likely to enjoy it. Of course, if you enjoy it, you're more likely to leave a positive review than a negative one. The point is that the vast majority of people who would leave a negative review do not do so, because they do not see or do not buy the game in the first place.
What we want the score to mean is if a random person was forced to buy, play, and leave a review, then how likely is that review to be positive. For that, the aforementioned biases must be eliminated. I am not aware of any method to do this, however.
We do know that all the biases are positive, so our score should be lower than the real score. We can also assume that games with more reviews have reached a more varied audience, so the biases are not as severe as they are for games with fewer reviews. Of course, the review count is not the only observable aspect correlating with the size of the bias, but I believe it is by far the simplest, and on average with the best correlation, so it is the only one I use.

  • The main question I have is what the monotonously decreasing function that maps a review count to a "bias percentage" should be. I currently use the inverse cube root of the review count. This is the "Default" scoring formula.
    score = positiveReviews / totalReviews * (1 - (totalReviews + 1) ** -(1 / 3))
    We can otherwise understand this as giving a game with no reviews a default score of 0. For every 8 times the review count increases, the default score becomes half as relevant, and the real score becomes that much more relevant.
  • I didn't use to believe a negative bias should be applied to all scores. The "Old Default" formula is otherwise the same as Default but assumes a default score of 50% and decreases relevancy of that every time the review count increases 16, not 8, times.
    score = positiveReviews / totalReviews - (positiveReviews / totalReviews - 0.5) * (totalReviews + 1) ** -0.25
    I find this ranks many games with very few reviews too highly, while simultaneously caring too much for the review count among games with a lot of reviews.
  • While I never used it, one of the earlier ideas I had was based on additive smoothing, also known as Laplace smoothing.
    score = (positiveReviews / totalReviews * log10(totalReviews + 1) + 0.5) / (log10(totalReviews + 1) + 1)
    It 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.
    This is where I first got the idea of putting the review counts on a logarithmic scale, which I essentially still use.
  • I have also added an experimental formula, "Phase out old", which is otherwise identical to Default, but which slowly lowers the score of games that are older than 20 years. It is a controversial topic, but I believe that games especially are a new enough medium that older games suffer not just from being less aligned with current tastes, but also from the technological limitations of the time. If the games were released today, they would receive lower ratings, and this formula is a conservative estimate of that. It is a final multiplier of
    6400 / (9 * age2 - 360 * age + 10000)

For all of the listed formulae, a monotonic function is applied to stretch the score to better cover the 0-100% range, for your viewing pleasure.
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.