Free Cloud Games Finder: 13 Steps, 60 Min [2026]

Free cloud games stopped being one thing this autumn. Microsoft now meters Xbox cloud play at 5, 10 and 15 hours a month depending on the Game Pass tier. GeForce NOW keeps a Free membership that streams games from libraries you already own. Amazon’s November 5 content update lists more than 80 games for Luna Standard.

That split turns a simple question into a chore: which of the free cloud games on my wishlist can I play for $0 tonight, and on which service? This tutorial builds a small Python tool that answers it. It reads your wishlist, three catalog exports and your owned-games list, then prints the cheapest route for every title. If you would rather compare the services first, the Luna, GeForce NOW and Xbox side-by-side is a good warm-up.

The build takes 13 steps and about 60 minutes. It uses only the Python standard library, so there is nothing to install beyond Python itself. I ran every command and every output block below on Python 3.12.3, and the sample data uses titles from this week’s announcements. Facts about plans and limits are current as of November 6, 2026.

Google · Preferred Sources

Don’t miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Counts as a Free Cloud Game in November 2026

“Free” now means three different things, and the tool you are about to build has to model all of them. A game can be free to play, free because you already own it, or free because a subscription you pay for anyway includes it. Each service leans on a different one of those ideas, and each meters your time differently.

GeForce NOW sells three membership names: Free, Performance and a top tier. Its catalog works on ownership. NVIDIA’s own materials describe more than 2,000 supported PC games that stream from your existing store libraries, plus more than 100 free-to-play titles. Paid tiers include 100 hours of monthly playtime, with up to 15 unused hours rolling into the next month, and a Day Pass buys 24 consecutive hours of paid-tier benefits. The Free tier is not an all-you-can-play catalog. It is access to supported games you own or that cost nothing, behind a queue, an ad and a timed session. Reports differ on the exact session length, so the tool keeps it as a number you set. If you want the Steam-linking side of that story, the GeForce NOW Free tier setup walks through it.

Xbox went the other way. Cloud play used to ride along with Game Pass without a stated cap. Since the November change, the Premium plan page lists 10 cloud hours per month, Essential gets 5 and the top tier gets 15. On October 21, Xbox began an Insider test of an ad-supported free option that needs no Game Pass subscription, and of prepaid hour packs at introductory prices of 10 hours for $3.99, 25 hours for $7.99 and 50 hours for $11.99. Both were still previews, so treat them as region-dependent. For the no-subscription route in detail, see the Xbox Cloud Gaming without Game Pass walkthrough.

Luna is the simplest of the three. Amazon’s November content update says more than 80 games are available through Luna Standard and that the new November titles cost Prime members nothing extra. I could not confirm current prices for any other Luna plan, so the tool models Prime and nothing else. The Luna free tier setup guide covers the account side.

Service What “free” means Catalog model Metering
GeForce NOW Free Free membership, timed sessions, queue and an ad Games you own on linked stores, plus 100+ free-to-play titles Paid tiers: 100 hours a month, 15 roll over. Day Pass: 24 hours
Xbox Cloud Gaming Insider-only ad-supported test since Oct. 21, 2026 Game Pass tiers: Essential 50+ games, Premium 200+, top tier 500+ Essential 5 h, Premium 10 h, top tier 15 h a month. Insider packs from $3.99
Amazon Luna Included with Prime for the November titles Luna Standard, 80+ games per Amazon No monthly hour figure in the sources I checked

Notice what the table implies for a wishlist tool. Being in a catalog is not the same as being playable for free. A GeForce NOW listing without ownership is a purchase. An Xbox listing without the right tier is a subscription. Only the status column the tool computes can tell you which is which.

What You Will Build and What You Need First

The finished project is a command-line program called finder.py with a small package behind it. You feed it five plain-text files and it prints a table with one row per wishlist game, the best free route and the reason. It also warns you when an export has gone stale or when your wishlist would blow through the Xbox hours your plan includes.

Requirement Version or detail Why it matters
Python 3.10 or newer (built and run on 3.12.3) The code uses the X | None type syntax from 3.10
Operating system Windows 11, macOS or Linux No platform-specific calls anywhere
Extra packages None, standard library only difflib, csv, json, argparse and unittest ship with Python
Accounts NVIDIA, Microsoft and Amazon (Prime for Luna) You copy catalog lists from each service while signed in
Editor Any text editor Everything is plain text, CSV or JSON
Time About 60 minutes 25 minutes of code, 20 of data entry, 15 of testing

Where does it run? Anywhere Python runs. On a Chromebook that means the Linux development environment, if your model offers one, so check Google’s current list before you plan around it. The Chromebook, TV and old laptop walkthrough covers the streaming side. This tool only plans what to stream, so any laptop works. Here is the layout you will end up with.

cloud-finder/
  config.json
  wishlist.csv
  owned.txt
  catalogs/
    geforcenow.csv
    xbox_cloud.csv
    luna_prime.txt
  finder/
    __init__.py
    normalize.py
    matcher.py
    catalogs.py
    routes.py
  tests/
    test_finder.py
  finder.py

Steps 1 and 2: Create the Project and the Config File

Step 1: make the folder and a virtual environment. The virtual environment is optional because the project has no dependencies, but it keeps you honest if you add a fuzzy-matching library later. Run these on macOS or Linux.

mkdir -p cloud-finder/finder cloud-finder/catalogs cloud-finder/tests
cd cloud-finder
python3 -m venv .venv
source .venv/bin/activate
touch finder/__init__.py
python3 --version

On Windows, activate with .venv\Scripts\activate instead, and use py -3 in place of python3. The version command should print 3.10 or higher. If it prints 3.9 or lower, upgrade before going on, because the type hints in later files will raise a syntax error.

Step 2: write config.json. Every knob lives here so you never edit code to change your situation. The fuzzy threshold decides how similar two titles must be to count as the same game. The Xbox plan and the Prime flag describe what you pay for today. The session minutes value is a placeholder, so read the timer your own Free session shows and replace it.

{
  "fuzzy_threshold": 0.88,
  "xbox_plan": "none",
  "has_prime": true,
  "gfn_free_session_minutes": 30,
  "max_catalog_age_days": 14,
  "xbox_hours": {"essential": 5, "premium": 10, "top": 15},
  "aliases": {}
}

The hour allowances match the figures on Xbox’s plan pages. The tier called top here is Game Pass’s highest plan, which I renamed in code so the key stays short. Set xbox_plan to essential, premium or top once you know which one you hold, and leave it at none if you have no Game Pass.

Step 3: Export Your Wishlist, Owned Games and Catalogs

This is the step that takes the longest, and it is the one most people try to skip with a scraper. Don’t. The services change their pages without warning, and scraping a logged-in account can break the terms you agreed to. I also did not find a documented export button for these lists in the sources I checked, so copying titles by hand is the reliable route. It takes ten minutes once and a few minutes each week after that.

Where to copy from: the games list on NVIDIA’s GeForce NOW page for the supported titles, the Library tab in Xbox’s refreshed web experience for Game Pass cloud games (announced November 5 and rolling out over the following weeks, it merges the games you own with Game Pass titles and lets you filter), and Amazon’s Luna page for the Prime-included list. Your owned games come from the libraries you linked to GeForce NOW. Paste one title per line, then add the export date as a comment on the first line. The finder reads that date to warn you when the data ages out.

Start with the wishlist and the owned list. The hours column is how many hours a month you expect to play each game, and it feeds the Xbox budget in step 11.

# wishlist.csv
title,hours_per_month
Hogwarts Legacy,10
Phantom Blade Zero,8
Cyberpunk 2077 Collection,6
Witcher 3 Wild Hunt Remastered,4
Void Breaker,7
Vampire Crawlers,3
MySims Cozy Bundle,2
Sample Free Arena,12
Sample Prime Puzzler,2
Call of Duty: Modern Warfare 3,5
Totally Unknown Game,1

# owned.txt
# exported: 2026-11-05
Cyberpunk 2077
Hogwarts Legacy

Now the three catalogs. The sample rows come from this week’s announcements, so the titles are real, but the tier and free-to-play columns are placeholders for the demo. Fill yours from the actual pages. Sample Free Arena and the two Sample rows for Luna are invented on purpose, so nobody mistakes demo data for a claim about a real game.

# catalogs/geforcenow.csv
# exported: 2026-11-05
# Demo rows. Replace with titles copied from NVIDIA's own games list.
title,f2p
Hogwarts Legacy,no
Phantom Blade Zero,no
Call of Duty: Modern Warfare 4,no
World of Warcraft: Forever,no
Cyberpunk 2077,no
The Witcher 3: Wild Hunt - Remastered,no
Sample Free Arena,yes

# catalogs/xbox_cloud.csv
# exported: 2026-11-05
# Demo rows. The tier column is a placeholder: copy yours from the Library tab.
title,tier
Void/Breaker,premium
Vampire Crawlers: The Turbo Wildcard from Vampire Survivors,premium
MySims Cozy Bundle,essential
Thronefall,top
Sample Free Arena,free

# catalogs/luna_prime.txt
# exported: 2026-11-05
# Demo rows. Replace with titles from Amazon's Luna page.
Sample Prime Puzzler
Sample Party Night

Notice that the wishlist spells some games differently from the catalogs: Void Breaker without the slash, Witcher 3 without the colon and the word The, Vampire Crawlers without its long subtitle. That is deliberate. Real exports never agree with each other, and the next three steps exist to cope with it.

Step 4: Normalize Titles So Matching Has a Fair Chance

Matching raw strings fails on trademark symbols, accents, punctuation and edition words. The fix is to reduce every title to a plain lowercase key before comparing anything. Create finder/normalize.py with the function below.

import re
import unicodedata

ROMAN = {"ii": "2", "iii": "3", "iv": "4", "vi": "6", "vii": "7", "viii": "8", "ix": "9"}
NOISE = {"edition", "deluxe", "goty", "complete", "bundle", "standard"}


def normalize(title: str) -> str:
    text = unicodedata.normalize("NFKD", title)
    text = "".join(ch for ch in text if not unicodedata.combining(ch))
    text = text.lower().replace("&", " and ")
    text = re.sub(r"[™®©]", "", text)
    text = re.sub(r"[^a-z0-9]+", " ", text).strip()
    tokens = text.split()
    if tokens and tokens[0] == "the":
        tokens = tokens[1:]
    cleaned = []
    for i, tok in enumerate(tokens):
        if tok in NOISE:
            continue
        if i > 0 and tok in ROMAN:
            tok = ROMAN[tok]
        cleaned.append(tok)
    return " ".join(cleaned)

Three choices deserve a comment. The function drops edition words like deluxe and goty because the same game ships under many labels. It keeps remastered, because a remaster and its original are different products with different catalog entries. And it converts Roman numerals to digits only after the first word, and only for the unambiguous ones. A lone V or X is skipped on purpose, since X is a legitimate word in titles like Mega Man X.

Quick check in a Python shell: normalize(“The Witcher® 3: Wild Hunt”) returns witcher 3 wild hunt, and normalize(“Civilization VI”) returns civilization 6. If both print as expected, the foundation works.

Step 5: Match Fuzzy Titles Without Mixing Up Sequels

Normalizing gets you most of the way. The rest is fuzzy comparison. Python’s difflib.SequenceMatcher gives a similarity ratio between 0 and 1 with no installs. The catch is that it scores Modern Warfare 3 and Modern Warfare 4 as nearly identical, which is high enough to count as the same game under almost any sensible threshold. Telling the user their wishlist game is on a service when only its sequel is would be the worst possible bug for this tool.

The guard is simple: compare the digits in each title first, and skip any catalog entry whose digit set differs. Create finder/matcher.py. It holds two functions, best_match for real decisions and closest for the hints you will see in step 9.

import re
from difflib import SequenceMatcher

from .normalize import normalize


def _numbers(text: str) -> set[str]:
    return set(re.findall(r"\d+", text))


def best_match(title: str, catalog: dict[str, str], threshold: float):
    """Return (catalog_key, score) for the closest catalog title, or (None, 0.0)."""
    wanted = normalize(title)
    if wanted in catalog:
        return wanted, 1.0
    wanted_numbers = _numbers(wanted)
    best_key, best_score = None, 0.0
    for key in catalog:
        if _numbers(key) != wanted_numbers:
            continue
        score = SequenceMatcher(None, wanted, key).ratio()
        if score > best_score:
            best_key, best_score = key, score
    if best_score >= threshold:
        return best_key, round(best_score, 3)
    return None, 0.0


def closest(title: str, catalog: dict[str, str]):
    """Best fuzzy score ignoring the threshold, used only for hints."""
    wanted = normalize(title)
    best_key, best_score = None, 0.0
    for key in catalog:
        score = SequenceMatcher(None, wanted, key).ratio()
        if wanted in key or key in wanted:
            score = max(score, 0.9)
        if score > best_score:
            best_key, best_score = key, score
    return best_key, round(best_score, 2)

The closest function ignores the threshold and the digit guard on purpose. It exists to explain a miss, not to make a match. It also boosts any title that contains the other as a substring, which rescues the case where a catalog adds a long subtitle to a short name. The hint is advice for a human, and the human decides whether it is the same game.

Step 6: Load the Catalogs and Add a Freshness Guard

Catalog data goes stale quickly. NVIDIA announced 25 games for GeForce NOW in October and 24 for November, and its weekly GFN Thursday posts keep adding titles between those announcements. Xbox publishes Game Pass additions in regular waves, such as the November wave announced on November 3 that included Void/Breaker and Vampire Crawlers. A catalog file from three weeks ago is a confident lie.

The loader below reads each file, pulls the export date from the comment line and returns the entries keyed by normalized title. It also keeps the original title so hints can show readable names. Create finder/catalogs.py.

import csv
import datetime as dt
from pathlib import Path

from .normalize import normalize


def _rows(path: Path):
    exported = None
    lines = []
    for raw in path.read_text(encoding="utf-8-sig").splitlines():
        line = raw.strip()
        if line.lower().startswith("# exported:"):
            exported = dt.date.fromisoformat(line.split(":", 1)[1].strip())
        elif line and not line.startswith("#"):
            lines.append(line)
    return exported, lines


def load_catalog(path: Path, extra_column: str | None = None):
    """Return ({normalized_title: extra_value}, export_date, {normalized: original})."""
    exported, lines = _rows(path)
    entries, originals = {}, {}
    if extra_column is None:
        for title in lines:
            entries[normalize(title)] = ""
            originals[normalize(title)] = title
    else:
        for row in csv.DictReader(lines):
            key = normalize(row["title"])
            entries[key] = row[extra_column].strip().lower()
            originals[key] = row["title"].strip()
    return entries, exported, originals


def load_wishlist(path: Path):
    _, lines = _rows(path)
    wishlist = []
    for row in csv.DictReader(lines):
        wishlist.append((row["title"].strip(), float(row.get("hours_per_month") or 0)))
    return wishlist


def stale_warning(name: str, exported, max_age_days: int, today: dt.date):
    if exported is None:
        return f"{name}: no '# exported: YYYY-MM-DD' line, age unknown"
    age = (today - exported).days
    if age > max_age_days:
        return f"{name}: exported {age} days ago (limit {max_age_days})"
    return None

Two details are worth knowing. The file is read with utf-8-sig, which quietly removes the invisible byte-order mark that Excel adds when it saves a CSV. Without that, your first column would be named \ufefftitle and every load would fail with a KeyError. And stale_warning treats a missing date the same as an old one, so a file you forgot to stamp never passes silently.

Step 7: Turn Matches Into Routes With an Access Model

Now the logic that answers the real question. For each service, a function takes a wishlist title and returns a Route with a status. The statuses form a ranking, and the cheapest wins. Free-to-play beats owned, owned beats included, and included beats anything that needs money. The ranking also encodes a habit worth keeping: if you own a game that GeForce NOW supports, play it there and save your metered Xbox hours for games you cannot get any other way. The GeForce NOW membership tiers article explains why the paid tiers exist if the Free route’s queue starts to hurt.

Status Meaning What you do
free Free-to-play on that service Launch it, no purchase or subscription needed
owned Supported and already in your linked library Launch it on the Free tier
included Covered by your Game Pass tier or Prime Launch it, and mind the monthly hours on Xbox
needs-tier Listed, but your plan is too low Upgrade a tier or skip the game
buy-first Supported on GeForce NOW, but you do not own it Buy it on a linked store first
none Not in any export you provided Check the closest hint, add an alias or accept the miss

Create finder/routes.py with the three route functions and the ranking helper.

from dataclasses import dataclass

from .matcher import best_match

RANK = {"free": 0, "owned": 1, "included": 2, "needs-tier": 3, "buy-first": 4, "none": 5}
TIER_ORDER = {"none": 0, "essential": 1, "premium": 2, "top": 3}


@dataclass
class Route:
    service: str
    status: str
    note: str
    uses_xbox_hours: bool = False


def gfn_route(title, catalog, owned, threshold):
    key, _ = best_match(title, catalog, threshold)
    if key is None:
        return Route("GeForce NOW Free", "none", "not in your exported list")
    if catalog[key] == "yes":
        return Route("GeForce NOW Free", "free", "free-to-play, no purchase needed")
    if best_match(title, owned, threshold)[0]:
        return Route("GeForce NOW Free", "owned", "supported and in your linked library")
    return Route("GeForce NOW Free", "buy-first", "supported, but you must own it first")


def xbox_route(title, catalog, plan, threshold):
    key, _ = best_match(title, catalog, threshold)
    if key is None:
        return Route("Xbox Cloud Gaming", "none", "not in your exported list")
    tier = catalog[key]
    if tier == "free":
        return Route("Xbox Cloud Gaming", "free", "free-to-play, check ad-supported access in your region")
    if TIER_ORDER[plan] >= TIER_ORDER.get(tier, 99):
        return Route("Xbox Cloud Gaming", "included", f"included with tier {tier} or higher", True)
    return Route("Xbox Cloud Gaming", "needs-tier", f"needs Game Pass tier {tier} or higher")


def luna_route(title, catalog, has_prime, threshold):
    key, _ = best_match(title, catalog, threshold)
    if key is None:
        return Route("Amazon Luna", "none", "not in your exported list")
    if has_prime:
        return Route("Amazon Luna", "included", "included with Prime in your export")
    return Route("Amazon Luna", "needs-tier", "needs an Amazon Prime membership")


def best_route(routes):
    return min(routes, key=lambda r: RANK[r.status])

The Xbox function is the only one that sets uses_xbox_hours. That flag feeds the budget check later, and it is why a game that is both owned on GeForce NOW and included with Game Pass reports GeForce NOW as the winner and burns no Xbox hours. If you hold no Game Pass, the plan value none gives a rank of zero and every tiered game reports needs-tier.

Step 8: Write the Command-Line Report

The last file ties everything together. finder.py loads the config, the five data files and the aliases, runs every wishlist title through the three route functions, picks a winner and prints a Markdown table. When no service matches, it swaps in a hint from the closest function so the miss explains itself. It also accepts a –today flag, which makes the freshness guard testable without waiting two weeks.

import argparse
import datetime as dt
import json
from pathlib import Path

from finder.catalogs import load_catalog, load_wishlist, stale_warning
from finder.matcher import closest
from finder.routes import Route, best_route, gfn_route, luna_route, xbox_route


def run(base: Path, today: dt.date):
    cfg = json.loads((base / "config.json").read_text(encoding="utf-8"))
    threshold = cfg["fuzzy_threshold"]
    gfn, gfn_date, gfn_names = load_catalog(base / "catalogs/geforcenow.csv", "f2p")
    xbox, xbox_date, xbox_names = load_catalog(base / "catalogs/xbox_cloud.csv", "tier")
    luna, luna_date, luna_names = load_catalog(base / "catalogs/luna_prime.txt")
    everything = {**gfn_names, **xbox_names, **luna_names}
    aliases = cfg.get("aliases", {})
    owned, _, _ = load_catalog(base / "owned.txt")
    wishlist = load_wishlist(base / "wishlist.csv")

    warnings = [w for w in (
        stale_warning("geforcenow.csv", gfn_date, cfg["max_catalog_age_days"], today),
        stale_warning("xbox_cloud.csv", xbox_date, cfg["max_catalog_age_days"], today),
        stale_warning("luna_prime.txt", luna_date, cfg["max_catalog_age_days"], today),
    ) if w]

    results, xbox_hours_used = [], 0.0
    for title, hours in wishlist:
        lookup = aliases.get(title, title)
        routes = [
            gfn_route(lookup, gfn, owned, threshold),
            xbox_route(lookup, xbox, cfg["xbox_plan"], threshold),
            luna_route(lookup, luna, cfg["has_prime"], threshold),
        ]
        winner = best_route(routes)
        if winner.status == "none":
            key, score = closest(lookup, everything)
            hint = f"closest: '{everything[key]}' ({score})" if key else "no similar title"
            winner = Route("-", "none", hint)
        if winner.uses_xbox_hours:
            xbox_hours_used += hours
        results.append({"title": title, "hours": hours, "best": winner, "all": routes})

    allowance = cfg["xbox_hours"].get(cfg["xbox_plan"], 0)
    if xbox_hours_used > allowance:
        warnings.append(
            f"Xbox hours: wishlist needs {xbox_hours_used:g} h/month, "
            f"plan '{cfg['xbox_plan']}' includes {allowance} h"
        )
    return results, warnings, cfg


def render(results, warnings, cfg) -> str:
    lines = ["| Game | Best free route | Status | Why |", "|---|---|---|---|"]
    for r in results:
        b = r["best"]
        lines.append(f"| {r['title']} | {b.service if b.status != 'none' else '-'} | {b.status} | {b.note} |")
    lines.append("")
    lines.append(f"GeForce NOW Free session cap in config: {cfg['gfn_free_session_minutes']} min")
    for w in warnings:
        lines.append(f"WARNING: {w}")
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Match a wishlist to free cloud gaming routes")
    parser.add_argument("--base", default=".", help="project folder")
    parser.add_argument("--today", default=None, help="override date, YYYY-MM-DD")
    parser.add_argument("--json", dest="json_out", default=None, help="write results to this file")
    args = parser.parse_args()

    today = dt.date.fromisoformat(args.today) if args.today else dt.date.today()
    results, warnings, cfg = run(Path(args.base), today)
    print(render(results, warnings, cfg))

    if args.json_out:
        payload = [
            {"title": r["title"], "service": r["best"].service,
             "status": r["best"].status, "note": r["best"].note}
            for r in results
        ]
        Path(args.json_out).write_text(json.dumps(payload, indent=2), encoding="utf-8")


if __name__ == "__main__":
    main()

Two design notes. Aliases are applied before matching, but the report still prints the title you typed, so your wishlist stays readable. And the Xbox budget counts hours only for games where Xbox is the winning route, which keeps the warning honest instead of alarmist.

Steps 9 and 10: Run It, Read the Output, Fix the Misses

Step 9: the first run

With xbox_plan set to none and no aliases, run the tool from the project root. The –today flag pins the date so your output matches mine.

python3 finder.py --today 2026-11-06
| Game | Best free route | Status | Why |
|---|---|---|---|
| Hogwarts Legacy | GeForce NOW Free | owned | supported and in your linked library |
| Phantom Blade Zero | GeForce NOW Free | buy-first | supported, but you must own it first |
| Cyberpunk 2077 Collection | - | none | closest: 'Cyberpunk 2077' (0.9) |
| Witcher 3 Wild Hunt Remastered | GeForce NOW Free | buy-first | supported, but you must own it first |
| Void Breaker | Xbox Cloud Gaming | needs-tier | needs Game Pass tier premium or higher |
| Vampire Crawlers | - | none | closest: 'Vampire Crawlers: The Turbo Wildcard from Vampire Survivors' (0.9) |
| MySims Cozy Bundle | Xbox Cloud Gaming | needs-tier | needs Game Pass tier essential or higher |
| Sample Free Arena | GeForce NOW Free | free | free-to-play, no purchase needed |
| Sample Prime Puzzler | Amazon Luna | included | included with Prime in your export |
| Call of Duty: Modern Warfare 3 | - | none | closest: 'Call of Duty: Modern Warfare 4' (0.97) |
| Totally Unknown Game | - | none | closest: 'Call of Duty: Modern Warfare 4' (0.41) |

GeForce NOW Free session cap in config: 30 min

Read it row by row. Hogwarts Legacy is owned, so it plays on the Free tier. Phantom Blade Zero is supported but not in your library, so it reports buy-first. The two Xbox rows say needs-tier because the plan is none. Sample Free Arena wins as free-to-play. Four rows say none, and they teach different lessons. Cyberpunk 2077 Collection and Vampire Crawlers are false misses: the hint names the right catalog entry. Call of Duty: Modern Warfare 3 shows a high-scoring hint, but that is the sequel, and the digit guard correctly refused it. Totally Unknown Game is a real miss.

Step 10: add aliases and set your plan

Fix the two false misses by mapping your wording to the catalog wording, and set the plan to premium to simulate holding that tier. Edit config.json like this.

  "xbox_plan": "premium",
  ...
  "aliases": {
    "Cyberpunk 2077 Collection": "Cyberpunk 2077",
    "Vampire Crawlers": "Vampire Crawlers: The Turbo Wildcard from Vampire Survivors"
  }
| Game | Best free route | Status | Why |
|---|---|---|---|
| Hogwarts Legacy | GeForce NOW Free | owned | supported and in your linked library |
| Phantom Blade Zero | GeForce NOW Free | buy-first | supported, but you must own it first |
| Cyberpunk 2077 Collection | GeForce NOW Free | owned | supported and in your linked library |
| Witcher 3 Wild Hunt Remastered | GeForce NOW Free | buy-first | supported, but you must own it first |
| Void Breaker | Xbox Cloud Gaming | included | included with tier premium or higher |
| Vampire Crawlers | Xbox Cloud Gaming | included | included with tier premium or higher |
| MySims Cozy Bundle | Xbox Cloud Gaming | included | included with tier essential or higher |
| Sample Free Arena | GeForce NOW Free | free | free-to-play, no purchase needed |
| Sample Prime Puzzler | Amazon Luna | included | included with Prime in your export |
| Call of Duty: Modern Warfare 3 | - | none | closest: 'Call of Duty: Modern Warfare 4' (0.97) |
| Totally Unknown Game | - | none | closest: 'Call of Duty: Modern Warfare 4' (0.41) |

GeForce NOW Free session cap in config: 30 min
WARNING: Xbox hours: wishlist needs 12 h/month, plan 'premium' includes 10 h

Cyberpunk now resolves to owned, and both Xbox rows flip to included. The Warning line at the bottom is the budget check firing, which the next section explains. Do not alias the Modern Warfare row. The two games are different, and an alias would hide a real gap.

Steps 11 to 13: Budget Xbox Hours, Test the Matcher, Schedule It

Step 11: read the hours budget and the freshness warning

The warning says the wishlist needs 12 hours a month of Xbox cloud play while the Premium plan includes 10. Void Breaker takes 7, Vampire Crawlers 3 and MySims Cozy Bundle 2. You have three choices: trim hours, move a game to a route that does not meter time, or buy extra hours. The Insider packs (Polygon covered the pay-as-you-go pricing) start at $3.99 for 10 hours and would cover the gap, but they were a preview as of October 21, so do not budget around them yet. If you want a live counter rather than a plan, the Xbox cloud hours tracker builds one.

Now test the freshness guard. Pretend it is three weeks later by passing a later date.

python3 finder.py --today 2026-11-30
WARNING: geforcenow.csv: exported 25 days ago (limit 14)
WARNING: xbox_cloud.csv: exported 25 days ago (limit 14)
WARNING: luna_prime.txt: exported 25 days ago (limit 14)
WARNING: Xbox hours: wishlist needs 12 h/month, plan 'premium' includes 10 h

Those three lines are the reason the tool exists in its current shape. A table of confident answers built on 25-day-old data is worse than no table.

Step 12: lock the behavior with tests

The two bugs most likely to creep back are the sequel cross-match and the punctuation drift. Pin both with unittest, which needs no install. Save this as tests/test_finder.py.

import unittest

from finder.matcher import best_match
from finder.normalize import normalize


class NormalizeTests(unittest.TestCase):
    def test_strips_marks_and_edition_words(self):
        self.assertEqual(normalize("The Witcher® 3: Wild Hunt"), "witcher 3 wild hunt")
        self.assertEqual(normalize("Cyberpunk 2077 Deluxe Edition"), "cyberpunk 2077")

    def test_roman_numerals_become_digits(self):
        self.assertEqual(normalize("Civilization VI"), "civilization 6")


class MatcherTests(unittest.TestCase):
    catalog = {normalize(t): "" for t in ["Call of Duty: Modern Warfare 4", "Void/Breaker"]}

    def test_sequel_numbers_never_cross_match(self):
        key, _ = best_match("Call of Duty: Modern Warfare 3", self.catalog, 0.5)
        self.assertIsNone(key)

    def test_punctuation_drift_still_matches(self):
        key, score = best_match("Void Breaker", self.catalog, 0.88)
        self.assertEqual(key, "void breaker")
        self.assertGreaterEqual(score, 0.88)


if __name__ == "__main__":
    unittest.main()
python3 -m unittest -v tests.test_finder
test_punctuation_drift_still_matches (tests.test_finder.MatcherTests) ... ok
test_sequel_numbers_never_cross_match (tests.test_finder.MatcherTests) ... ok
test_roman_numerals_become_digits (tests.test_finder.NormalizeTests) ... ok
test_strips_marks_and_edition_words (tests.test_finder.NormalizeTests) ... ok

Ran 4 tests in 0.001s

OK

Step 13: schedule a weekly run

NVIDIA adds games on Thursdays through its GFN Thursday posts, so run the finder on Friday morning after you refresh the exports. On Linux or macOS, add this line with crontab -e. On Windows, the schtasks line below creates the same weekly job. The JSON file it writes pairs well with the data use tracker if you also watch your monthly bandwidth.

# cron: every Friday at 08:00
0 8 * * 5 cd ~/cloud-finder && python3 finder.py --json weekly.json >> finder.log 2>&1
schtasks /Create /SC WEEKLY /D FRI /ST 08:00 /TN "CloudFinder" /TR "python C:\cloud-finder\finder.py --json C:\cloud-finder\weekly.json"

You now have a weekly shortlist of free cloud games, and the project is working end to end: config, five data files, four modules, a CLI, tests and a schedule. Commit the folder to Git if you like. The catalog files make small, readable diffs, and a diff after each weekly refresh shows you exactly which games arrived or left.

Common Pitfalls That Skew the Results

Most wrong answers about free cloud games come from the data and the access model, not from the Python. These are the traps I hit or watched others hit.

  • Treating “supported” as “free.” A GeForce NOW listing means the game can stream. It says nothing about whether you own it. The buy-first status exists to stop this mistake.
  • Trusting a high fuzzy score on a sequel. Modern Warfare 3 against Modern Warfare 4 scores 0.97. Without the digit guard, the tool would send you to a game that is not there.
  • Using an old export. NVIDIA announced 24 GeForce NOW games for November alone. A list from last month misses most of them, and a list that quietly drops removed games misleads you the other way.
  • Ignoring the Xbox meter. Game Pass cloud play is no longer open-ended, so a game marked included can still cost you hours you do not have left.
  • Reading preview features as general availability. The ad-supported Xbox option and the hour packs were Insider tests as of October 21. Region and account eligibility decide whether you see them.
  • Hard-coding prices. Plan prices vary by region and change often, so the tool deliberately stores no prices. Check the official page on the day you subscribe.
  • Aliasing away real gaps. Every alias is a promise that two titles are the same game. If you alias a sequel to its predecessor to silence a none row, you are lying to yourself.

Troubleshooting: 10 Problems and Their Fixes

Each item lists the symptom, the likely cause and the fix. Work down the list in order, because the early items cover the most common failures.

  1. KeyError: ‘title’ when loading a CSV. The header row is missing or misspelled, or the first line is the export comment. Keep the comment lines starting with #, and make the header exactly title or title,f2p or title,tier.
  2. KeyError: ‘f2p’ or ‘tier’. You pointed a loader at a file with a different second column. The GeForce NOW file needs f2p, the Xbox file needs tier.
  3. Every title shows none. The catalogs are empty, or all rows are comments. Open one file and confirm real titles sit below the header.
  4. ValueError: Invalid isoformat string. The export line is not in YYYY-MM-DD form. Write # exported: 2026-11-05 with that exact shape.
  5. KeyError: ‘Premium’ from the routes module. Plan names are lowercase. Use premium, not Premium, and only essential, premium, top or none.
  6. ModuleNotFoundError: No module named ‘finder’. You ran the script from another folder. Change into the project root, or pass –base only after fixing your working directory, because imports resolve from where you launch Python.
  7. UnicodeEncodeError on Windows when printing titles. The console codepage cannot show a trademark symbol. Set the environment variable PYTHONUTF8=1 and run again.
  8. A game you own shows buy-first. The owned list uses different wording. Run the tool, read the hint or the title in your library, and add an alias or fix the spelling in owned.txt.
  9. A wrong game matched. Raise fuzzy_threshold toward 0.92, then re-run the tests. Lower it only if misses pile up, and review every new match by eye.
  10. The service itself will not load or connect. That is a network or account problem, not a finder bug. The GeForce NOW connection error guide covers the usual causes.

One more habit helps with all ten: when something looks wrong, run with a single wishlist row and a single catalog row. Shrinking the input to one pair exposes the cause in seconds.

Advanced Tips for a Sharper Finder

  • Swap in a faster matcher. If your catalogs reach thousands of rows, install the latest version of the rapidfuzz package and replace SequenceMatcher inside best_match. Keep the digit guard either way.
  • Add a priority column. Extend wishlist.csv with a priority from 1 to 3, then sort the report so priority 1 games always claim scarce Xbox hours first.
  • Split Free sessions. Use gfn_free_session_minutes to flag games you know run long. A game that needs 90 minutes to reach a save point is a poor fit for a short timed session.
  • Diff the exports. Keep the catalogs in Git and run git diff after each refresh. New lines are arrivals, and removed lines are departures you would otherwise miss.
  • Pipe the JSON onward. The –json output is a flat list. Feed it to jq, a spreadsheet or a Discord webhook so the Friday report finds you.
  • Add a fourth route. Another service is just one more catalog file and one more function in routes.py. Copy luna_route, change the labels and add it to the routes list in finder.py.

For more free services to feed into that fourth route, browse the gaming category. The same ownership-versus-subscription questions apply to nearly every provider.

FAQ: Free Cloud Games and the Finder

Is GeForce NOW Free really free?

The membership costs $0 and still exists under the name Free. The catch is that it streams games you own or free-to-play titles, behind a queue, an ad and timed sessions. Prices and limits are region-specific, so confirm them on NVIDIA’s page.

Do I need Game Pass to play Xbox cloud games for free?

Not always, but the free route was a preview as of October 21, 2026. Xbox began testing an ad-supported option for Insiders that needs no subscription. Outside that test, cloud play comes with a Game Pass tier and monthly hours. The Xbox Insider cloud test shows how the preview works.

Does Amazon Luna cost anything beyond Prime?

Amazon says the new November titles come at no additional cost to Prime members. I could not verify other plan names or prices, so the finder models Prime only. Check Amazon’s Luna page before you pay for anything else.

Can the script download the catalogs for me?

It can’t, by design. Pages change, accounts sit behind logins, and automated scraping can violate a service’s terms. Copying a list by hand takes minutes and keeps you on safe ground.

How often should I refresh the exports?

Weekly is a sensible rhythm for a list of free cloud games. NVIDIA posts additions in its weekly GFN Thursday updates and Xbox publishes Game Pass waves on a regular schedule. The max_catalog_age_days setting of 14 warns you before the data gets too old.

Will this run on a Chromebook or a phone?

It runs wherever Python 3.10 or newer runs. On a Chromebook that means a Linux environment if your device supports it. On a phone, run it on a PC and open the JSON or Markdown report.

Why use difflib instead of a fuzzy-matching library?

The standard library keeps the project to zero installs, which makes the tutorial portable. For large catalogs a dedicated library is faster, and the matcher is the only file you would change.

Marcus Chen

Marcus Chen

Gaming & Consumer Tech Editor

Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.

View all articles

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *