Backhander

How the game worksComment marche le jeu

The principle is simple

You run a cult. Each turn a card shows an event, with up to three ways to answer it. Every answer has a price in cards, which you pay from your hand: relics, money, cultists, food, prisoners. In return you get other cards, or the story moves on. Some answers lead to an Ancient God. Summon one and you win. Run out of ways to answer and you lose.

Suspicion is the only resource nobody wants. It comes with the answers that help you most, and when it grows too much the police come knocking.

What decompiling taught me

The wiki and the playthroughs describe the cards. The code describes the rules, and some of them are nowhere else.

  • Punishments are chances, not thresholds. Before each draw, the game rolls: 16 cards or more in hand, and Greed may come up (35 % at 16, certain at 21). Suspicion from 5, and the Police Raid (35 % at 5, certain at 10). No food, and Desperate Measures (20 %). The more you hoard, the more likely it is.
  • The deck is weighted, not shuffled flat. Each card has a weight that decides how early it tends to come out. The cards that start the story weigh a lot.
  • The game is finite. Only 26 of the 118 cards come back after being played. The rest leave the game, so a long game runs low on events.
  • No two games begin alike. Most starting cards are in the deck on a coin flip. A few are not left to chance: the first rumours are always in the first five cards.

I did not find all of this at once. Some of it is read straight in the code, some deduced and then checked by playing, and I marked which is which in my notes.

Le principe est simple

Vous dirigez une secte. À chaque tour, une carte montre un événement, avec jusqu’à trois façons d’y répondre. Chaque réponse a un prix en cartes, que vous payez avec votre main : reliques, argent, cultistes, nourriture, prisonniers. En échange, vous recevez d’autres cartes, ou l’histoire avance. Certaines réponses mènent à un Ancien. Invoquez-en un et vous gagnez. Plus aucune façon de répondre, et vous perdez.

La suspicion est la seule ressource dont personne ne veut. Elle vient avec les réponses qui vous aident le plus, et quand elle grossit trop, la police arrive.

Ce que la décompilation m’a appris

Le wiki et les parties filmées décrivent les cartes. Le code décrit les règles, et certaines ne sont écrites nulle part ailleurs.

  • Les punitions sont des chances, pas des seuils. Avant chaque tirage, le jeu lance un dé : 16 cartes ou plus en main, et l’Avarice peut sortir (35 % à 16, certaine à 21). Suspicion à partir de 5, et la Descente de police (35 % à 5, certaine à 10). Plus de nourriture, et les Mesures désespérées (20 %). Plus on accumule, plus c’est probable.
  • Le paquet est pondéré, pas mélangé à plat. Chaque carte a un poids qui décide de sa tendance à sortir tôt. Les cartes qui lancent l’histoire pèsent lourd.
  • Le jeu est fini. Seules 26 des 118 cartes reviennent après avoir été jouées. Les autres quittent la partie, si bien qu’une longue partie manque d’événements.
  • Deux parties ne commencent pas pareil. La plupart des cartes de départ sont dans le paquet à pile ou face. Quelques-unes ne sont pas laissées au hasard : les premières rumeurs sont toujours dans les cinq premières cartes.

Je n’ai pas tout trouvé d’un coup. Une partie est lue directement dans le code, une partie déduite puis vérifiée en jouant, et j’ai noté dans mes documents ce qui est quoi.

How a card worksComment fonctionne une carte

Each turn an event card is drawn. It offers one to three options. To play one, you drag cards from your hand onto the altar: the altar must pay the option’s price exactly. Your hand is made of resource cards (relic, money, cultist, food, prisoner, suspicion), with no counters and no ceiling. The relic is a wildcard.

A real card, the first one in the game, cut down a bit:

{ "id": 1, "weight": 10, "recurring": true,
  "startingDeck": [{ "chance": 0.5 }],
  "options": [
    { "id": 1, "cost": [{ "amount": 2, "accepts": ["money"] }],
                "rewards": { "food": 1 } },
    { "id": 2, "cost": [{ "amount": 2, "accepts": ["food"] }],
                "rewards": { "money": 1 } },
    { "id": 3 }
  ] }
FieldMeaning
weightChance of coming out early when the deck is shuffled. Higher is earlier.
recurringGoes back to the discard pile after being played, and so comes back. Otherwise it leaves the game.
costA list of slots, each with an exact amount and the resources it accepts. A slot can accept several (cultists or prisoners) or a group (two prisoners count as one cultist). Here: 2 money.
rewardsCards added to your hand. Here: 1 food. Option 3 has neither cost nor reward: it costs nothing and does nothing.
startingDeckWhether, and with what chance, the card is in the deck at the start of a game (see below).

An option can do more than swap cards:

  • addCards and addRandomCards slip other cards into the deck: that is how event chains are told.
  • foresight reveals the top cards of the deck, and may let you throw some away.
  • loseRandom takes cards from your hand at random after payment.
  • conditions reserve an option for some hands, for example “only if you have no food”.
  • outcome ends the game: summon a god, or lose. lastResort makes an option playable only when no other is.

The titles and texts are not on the card itself: they are in the translation files, looked up by the card’s id.

À chaque tour, une carte événement est tirée. Elle propose une à trois options. Pour en jouer une, on glisse des cartes de sa main sur l’autel : l’autel doit payer le prix de l’option exactement. La main est faite de cartes ressources (relique, argent, cultiste, nourriture, prisonnier, suspicion), sans compteur ni plafond. La relique est un joker.

Une vraie carte, la première du jeu, un peu raccourcie :

{ "id": 1, "weight": 10, "recurring": true,
  "startingDeck": [{ "chance": 0.5 }],
  "options": [
    { "id": 1, "cost": [{ "amount": 2, "accepts": ["money"] }],
                "rewards": { "food": 1 } },
    { "id": 2, "cost": [{ "amount": 2, "accepts": ["food"] }],
                "rewards": { "money": 1 } },
    { "id": 3 }
  ] }
ChampSens
weightChance de sortir tôt quand le paquet est mélangé. Plus c’est haut, plus c’est tôt.
recurringRetourne dans la défausse après avoir été jouée, et revient donc. Sinon elle quitte la partie.
costUne liste d’emplacements, chacun avec une quantité exacte et les ressources qu’il accepte. Un emplacement peut en accepter plusieurs (cultistes ou prisonniers) ou un groupe (deux prisonniers valent un cultiste). Ici : 2 argent.
rewardsCartes ajoutées à la main. Ici : 1 nourriture. L’option 3 n’a ni coût ni récompense : elle ne coûte rien et ne fait rien.
startingDeckSi la carte est dans le paquet au début de la partie, et avec quelle chance (voir plus bas).

Une option peut faire autre chose que changer des cartes :

  • addCards et addRandomCards glissent d’autres cartes dans le paquet : c’est ainsi que se racontent les chaînes d’événements.
  • foresight révèle les cartes du dessus du paquet, et peut laisser en écarter.
  • loseRandom retire des cartes de la main au hasard après le paiement.
  • conditions réserve une option à certaines mains, par exemple « seulement sans nourriture ».
  • outcome termine la partie : invoquer un dieu, ou perdre. lastResort rend une option jouable seulement quand aucune autre ne l’est.

Les titres et les textes ne sont pas sur la carte elle-même : ils sont dans les fichiers de traduction, retrouvés grâce à l’id de la carte.

The starting deckLe paquet de départ

The starting deck is part of the rules, not of the engine. The starting hand and the choices offered to the player (startingHand, startingHandChoice, startingDeckChoice) are in the pack’s rules.json, and the rules apply the startingDeck entries that each card carries. Another pack can start another way.

Hand

A normal game starts with 2 money, 2 cultists, 2 food and 2 prisoners, and no relic or suspicion. The tutorial starts with an empty hand.

Deck

Of the 118 cards, only 25 carry a startingDeck entry. The others reach the deck later, added by other cards. For each entry:

  • chance: the probability that the card is in the deck. Most are 0.5, a coin flip per card, so two games rarely start the same.
  • minSummonedGods: the card only joins once you have summoned that many gods. Two cards need 3, two need 5.
  • game: restricts the entry to a type of game. The tutorial’s first card is on top of the deck.
  • position.fromTop: instead of being shuffled in, the card is inserted at a depth drawn in a range. “Rumors of Darkness” is always 0 to 4 cards from the top, so the story always starts in the first turns.

A few cards are not left to chance (chance: 1): “Rumors of Darkness”, and the cards that open the god chains once enough gods are summoned. Then the blessings are added: before a game you tick up to 3 gods you have summoned, and the god of your previous game is added on top (hence “up to 4”). Each brings its cards.

Shuffle

The cards are drawn one by one without replacement, each weighted by its weight, which gives the order of the deck. When the draw pile runs out, the discard pile is shuffled back in. Before each draw the game checks for punishments (Greed, Police Raid, Desperate Measures), which go on top of the deck.

The discard pile

There are two piles: the draw pile, where the next cards wait, and the discard pile.

  • A card you have played goes to the discard pile only if it is recurring. Otherwise it leaves the game for good.
  • Cards that an option adds to the deck (the chains) do not go on top: in a normal game they go to the discard pile and only come out at the next reshuffle. That is what spaces out the episodes of a story. In the tutorial they go on top, so you see them right away.
  • Cards you throw away with foresight go to the discard pile too, even if they are not recurring. They are not lost, they come back at the next reshuffle.
  • When the draw pile is empty, the discard pile is shuffled into it, again weighted by weight, and a “Reshuffling deck from discard” banner shows. Foresight does the same if fewer cards are left than it needs.
  • Punishments are the exception: they go straight on top of the draw pile, ahead of the discard pile.

If both piles are empty, the original game had no case for it. Here the pack decides (deckExhausted): by default, the game is lost.

Le paquet de départ fait partie des règles, pas du moteur. La main de départ et les choix proposés au joueur (startingHand, startingHandChoice, startingDeckChoice) sont dans le rules.json du paquet, et les règles appliquent les entrées startingDeck que porte chaque carte. Un autre paquet peut démarrer autrement.

La main

Une partie normale commence avec 2 argent, 2 cultistes, 2 nourriture et 2 prisonniers, sans relique ni suspicion. Le tutoriel commence avec une main vide.

Le paquet

Sur les 118 cartes, 25 seulement portent une entrée startingDeck. Les autres arrivent dans le paquet plus tard, ajoutées par d’autres cartes. Pour chaque entrée :

  • chance : la probabilité que la carte soit dans le paquet. La plupart valent 0.5, une pièce lancée par carte, si bien que deux parties commencent rarement pareil.
  • minSummonedGods : la carte n’entre qu’une fois ce nombre de dieux invoqués. Deux cartes demandent 3 dieux, deux en demandent 5.
  • game : limite l’entrée à un type de partie. La première carte du tutoriel est au-dessus du paquet.
  • position.fromTop : au lieu d’être mêlée, la carte est insérée à une profondeur tirée dans un intervalle. « Rumeurs ténébreuses » est toujours à 0 à 4 cartes du dessus, si bien que l’histoire démarre toujours dans les premiers tours.

Quelques cartes ne sont pas laissées au hasard (chance: 1) : « Rumeurs ténébreuses », et les cartes qui ouvrent les chaînes de dieux une fois assez de dieux invoqués. Viennent ensuite les bénédictions : avant la partie, on coche jusqu’à 3 dieux déjà invoqués, et le dieu de la partie précédente s’ajoute en plus (d’où « jusqu’à 4 »). Chacun apporte ses cartes.

Le mélange

Les cartes sont tirées une à une sans remise, chacune pondérée par son weight, ce qui donne l’ordre du paquet. Quand la pioche est vide, la défausse est remélangée dedans. Avant chaque tirage, le jeu vérifie les punitions (Avarice, Descente de police, Mesures désespérées), qui se placent au-dessus du paquet.

La défausse

Il y a deux piles : la pioche, où attendent les cartes suivantes, et la défausse.

  • Une carte jouée va à la défausse seulement si elle est récurrente. Sinon elle quitte la partie pour de bon.
  • Les cartes qu’une option ajoute au paquet (les chaînes) ne vont pas sur le dessus : dans une partie normale, elles vont à la défausse et ne sortent qu’au prochain remélange. C’est ce qui espace les épisodes d’une histoire. Dans le tutoriel, elles vont sur le dessus, pour les voir tout de suite.
  • Les cartes que vous écartez avec la prévoyance vont aussi à la défausse, même si elles ne sont pas récurrentes. Elles ne sont pas perdues, elles reviennent au prochain remélange.
  • Quand la pioche est vide, la défausse y est remélangée, toujours pondérée par weight, et un bandeau « La défausse retourne dans le paquet » s’affiche. La prévoyance fait pareil s’il reste moins de cartes qu’il n’en faut.
  • Les punitions font exception : elles vont directement sur le dessus de la pioche, avant la défausse.

Si les deux piles sont vides, le jeu d’origine n’avait pas prévu ce cas. Ici, c’est le paquet qui décide (deckExhausted) : par défaut, la partie est perdue.

The Collection and the card in 3DLa collection et la carte en 3D

I added the Collection because I like looking at the cards. The game remembers the cards you meet. The Collection sorts them by family (events, chains, blessings, punishments), counts how many you have seen, and marks the new ones. The others stay face down, which gives you something to aim for.

Tap a card you have seen to read it: the picture, then each option with its cost and what it gives. The 3D button turns it into an object with rounded corners and a cream edge, under a light that glints on it. Drag to turn it, back included. Double tap to put it face up again. On a phone, tilting the device tilts the card.

J’ai ajouté la collection parce que j’aime bien voir les cartes. Le jeu retient les cartes que vous rencontrez. La Collection les range par famille (événements, chaînes, bénédictions, punitions), compte combien vous en avez vu, et signale les nouvelles. Les autres restent face cachée, ce qui donne un but.

Touchez une carte déjà vue pour la lire : l’illustration, puis chaque option avec son coût et ce qu’elle rapporte. Le bouton 3D en fait un objet aux coins arrondis et à la tranche crème, sous une lumière qui y accroche des reflets. On la fait tourner en glissant, dos compris. Un double toucher la remet face visible. Sur téléphone, incliner l’appareil incline la carte.

What changes from UnderhandCe qui change par rapport à Underhand

Quality of life

  • Starting deck screen: choose your starting resources and the cards of your deck, or leave it to chance.
  • A Collection that remembers the cards you have met, with each option’s cost and reward, and a 3D view of the card.
  • A pantheon per pack, with a sheet for every god and the blessings it gives.
  • Foresight can also reorder the next cards, keep one of them, or cap how many you cast aside.
  • A “Let it pass” button when no answer is playable, so that the game never gets stuck.
  • Chains and quests, with a medallion or a badge showing which god a chain leads to, banners when a quest starts or ends, and flags so that a story can branch.
  • Pause menu, a game in progress that resumes where you left it, the tutorial that can be replayed, and settings (volume, how chains are marked, a film grain on or off).
  • Translations: English and French, with fonts that have accents; Spanish and German are in progress, and any language can be added.
  • Portrait layout and touch controls for the phone.

Edge cases, and made clear

  • A few edge cases are handled: foresight with fewer than three cards in the deck, empty piles (the pack decides how the game ends), a random loss with an empty hand, a card of weight 0, and “Reset Game”, which resets everything.
  • The magic values of the original (a cost of 420, parallel arrays, positions used as ids) became named fields, so that a card can be read without a decoder.
  • What the original did without saying is written down: the payment must be exact, with no overpayment; a relic also pays suspicion costs; a random loss is uniform over the kinds of resource, not over the cards; a card cast aside by foresight goes to the discard pile.
  • Rules, content and texts are now separate files, and the game contains no card of its own.

Confort de jeu

  • Écran du paquet de départ : on choisit ses ressources de départ et les cartes de son paquet, ou on laisse faire le hasard.
  • Une collection qui retient les cartes rencontrées, avec le coût et la récompense de chaque option, et une vue 3D de la carte.
  • Un panthéon par paquet, avec une fiche pour chaque dieu et les bénédictions qu’il donne.
  • La clairvoyance peut aussi réordonner les cartes suivantes, en garder une seule, ou limiter le nombre de cartes écartées.
  • Un bouton « Laisser passer » quand aucune réponse n’est jouable, pour que la partie ne reste jamais bloquée.
  • Chaînes et quêtes, avec un médaillon ou un badge qui indique à quel dieu mène une chaîne, des bandeaux au début et à la fin d’une quête, et des drapeaux pour que l’histoire bifurque.
  • Menu pause, une partie en cours qui reprend où on l’a laissée, un tutoriel qu’on peut rejouer, et des réglages (volume, façon de marquer les chaînes, grain de film activé ou non).
  • Traductions : anglais et français, avec des polices accentuées ; l’espagnol et l’allemand sont en cours, et on peut ajouter n’importe quelle langue.
  • Mise en page en portrait et commandes tactiles pour le téléphone.

Cas limites, et rendu clair

  • Quelques cas limites sont gérés : la clairvoyance avec moins de trois cartes dans la pioche, les piles vides (le paquet décide comment la partie se termine), une perte au hasard avec une main vide, une carte de poids 0, et « Reset Game », qui remet tout à zéro.
  • Les valeurs magiques de l’original (un coût de 420, des tableaux parallèles, des positions servant d’identifiants) sont devenues des champs nommés : on peut lire une carte sans décodeur.
  • Ce que l’original faisait sans le dire est écrit : le paiement doit être exact, sans excédent ; une relique paie aussi les coûts de suspicion ; une perte au hasard est uniforme sur les sortes de ressources, pas sur les cartes ; une carte écartée par la clairvoyance va en défausse.
  • Règles, contenu et textes sont des fichiers séparés, et le jeu ne contient aucune carte en propre.

How the rules were recoveredComment les règles ont été retrouvées

After years of playing and reading the wiki and the community’s notes, I opened the APK. The Java side (JD-GUI) is only an Android shell. The game is C++, so I decompiled libmain.so with Ghidra and wrote down each rule in a document, tagging what was read in the code and what was only deduced. The port was written from that document.

Après des années à jouer et à lire le wiki et les notes de la communauté, j’ai ouvert l’APK. Le côté Java (JD-GUI) n’est qu’une coque Android. Le jeu est en C++ : j’ai donc décompilé libmain.so avec Ghidra et consigné chaque règle dans un document, en marquant ce qui était lu dans le code et ce qui n’était que déduit. Le portage a été écrit à partir de ce document.

The original formatLe format d’origine

Want to write your own cards? The modding page details the files of a pack: cards.json and everything an option can do, gods.json, the translations, and the rest.

Envie d’écrire vos propres cartes ? La page modding détaille les fichiers d’un paquet : cards.json et tout ce qu’une option peut faire, gods.json, les traductions, et le reste.

The original game keeps its 118 cards in one file, cardwip.json, about 280 KB long. It is the team’s working file, made to be filled in by hand by the people who wrote the game, and a readme explains how: “if something is the LAST field before closing braces, DO NOT put a comma; otherwise make sure you put a comma!!!” It does the job for a game, it just was not made for others to build on.

This is the first option of the first card, cut down to its essentials:

{ "1": {
    "title":      "Traveling Salesperson",
    "flavortext": "She probably doesn't know who she's selling to",
    "option1": {
      "optiontext": "    Buy Supplies",
      "outputtext": "",
      "cultistequalsprisoner": 0,
      "foresight": { "hasforesight": 0, "candiscard": 0 },
      "shuffle": { "lowerbound": 999, "upperbound": 999,
                   "specificids": [], "allowsdupes": 0, "numcards": 0 },
      "randomrequirements": 0,
      "requirements": { "relic": 0, "money": 2, "cultist": 0,
                        "food": 0, "prisoner": 0, "suspicion": 0 },
      "rewards":      { "relic": 0, "money": 0, "cultist": 0,
                        "food": 1, "prisoner": 0, "suspicion": 0 },
      "iswin": "",
      "islose": 0
    },
    "option2": { ... }, "option3": { ... },
    "isrecurring": 1, "isinitial": 1, "weight": 10, ... } }

What I had to untangle to build on it:

  • Every card carries every field, mostly at zero: costs and rewards for all six resources, a foresight block, a shuffle block, even when the option does nothing of the kind. Most of the file is zeros.
  • Not strict JSON: tabs inside strings, a key written twice, Windows line endings. The game’s own parser accepts it, so it never mattered to the game, only to other tools.
  • Special values instead of fields: a requirement of 420 means “half of what you hold”, 840 means “only if you have none”, 999 in a shuffle means “no shuffle”. The meaning lives in the code.
  • Parallel arrays: specificids and numcards must have the same length, and numcards is a number or a list depending on the case.
  • Booleans as 0 and 1, a win written as a god’s name in a string, a loss as islose: 1 plus a lock that lives in the code.
  • Always three options, empty ones included, to keep the layout. Ids are the position in the file.
  • Everything together: the texts sit in the data, the important cards are hard-coded by number in the program, and part of the text is baked into the pictures.

Rather than patching it, I read what each of those values meant in the decompiled code. Then I moved the cards from JSON to CSV, one row per option, which is far easier to read and check, and wrote the new JSON by hand from there: only the fields a card uses, named values, a list of cost slots, explicit ids, and the texts in separate translation files. The same card now looks like this:

{ "id": 1, "weight": 10, "recurring": true,
  "startingDeck": [{ "chance": 0.5 }],
  "options": [
    { "id": 1,
      "cost": [{ "amount": 2, "accepts": ["money"] }],
      "rewards": { "food": 1 } },
    ...
  ] }

Once the new format was stable, I wrote a script at the end to validate it and be sure of the result. A schema also checks every file. The hard-coded cards (Rumors of Darkness, the tutorial, the punishments) became ordinary data: startingDeck entries and rules.json.

Le jeu d’origine garde ses 118 cartes dans un seul fichier, cardwip.json, d’environ 280 Ko. C’est le fichier de travail de l’équipe, fait pour être rempli à la main par ceux qui ont écrit le jeu, et un fichier d’explications dit comment : « if something is the LAST field before closing braces, DO NOT put a comma; otherwise make sure you put a comma!!! » Ça fait très bien l’affaire pour un jeu, mais ce n’était pas fait pour que d’autres s’y appuient.

Voici la première option de la première carte, réduite à l’essentiel :

{ "1": {
    "title":      "Traveling Salesperson",
    "flavortext": "She probably doesn't know who she's selling to",
    "option1": {
      "optiontext": "    Buy Supplies",
      "outputtext": "",
      "cultistequalsprisoner": 0,
      "foresight": { "hasforesight": 0, "candiscard": 0 },
      "shuffle": { "lowerbound": 999, "upperbound": 999,
                   "specificids": [], "allowsdupes": 0, "numcards": 0 },
      "randomrequirements": 0,
      "requirements": { "relic": 0, "money": 2, "cultist": 0,
                        "food": 0, "prisoner": 0, "suspicion": 0 },
      "rewards":      { "relic": 0, "money": 0, "cultist": 0,
                        "food": 1, "prisoner": 0, "suspicion": 0 },
      "iswin": "",
      "islose": 0
    },
    "option2": { ... }, "option3": { ... },
    "isrecurring": 1, "isinitial": 1, "weight": 10, ... } }

Ce qu’il a fallu démêler pour s’appuyer dessus :

  • Chaque carte porte tous les champs, presque tous à zéro : coûts et récompenses des six ressources, un bloc de prévoyance, un bloc de mélange, même quand l’option n’a rien de tel. Le fichier est surtout fait de zéros.
  • Pas du JSON strict : des tabulations dans des chaînes, une clé écrite deux fois, des fins de ligne Windows. L’analyseur du jeu l’accepte, donc ça n’a jamais gêné le jeu, seulement les autres outils.
  • Des valeurs spéciales au lieu de champs : une exigence de 420 veut dire « la moitié de ce qu’on a », 840 « seulement si on n’en a aucun », 999 dans un mélange « pas de mélange ». Le sens est dans le code.
  • Des tableaux parallèles : specificids et numcards doivent avoir la même longueur, et numcards est un nombre ou une liste selon le cas.
  • Des booléens en 0 et 1, une victoire écrite par le nom d’un dieu dans une chaîne, une défaite par islose: 1 avec un verrou qui vit dans le code.
  • Toujours trois options, vides comprises, pour garder la mise en page. Les ids sont la position dans le fichier.
  • Tout ensemble : les textes sont dans les données, les cartes importantes sont codées en dur par leur numéro dans le programme, et une partie du texte est incrustée dans les images.

Plutôt que de rafistoler, j’ai lu dans le code décompilé ce que voulait dire chacune de ces valeurs. Puis j’ai porté les cartes du JSON vers du CSV, une ligne par option, bien plus facile à lire et à vérifier, et j’ai écrit le nouveau JSON à la main à partir de là : seulement les champs qu’une carte utilise, des valeurs nommées, une liste d’emplacements de coût, des ids explicites, et les textes dans des fichiers de traduction à part. La même carte ressemble maintenant à ceci :

{ "id": 1, "weight": 10, "recurring": true,
  "startingDeck": [{ "chance": 0.5 }],
  "options": [
    { "id": 1,
      "cost": [{ "amount": 2, "accepts": ["money"] }],
      "rewards": { "food": 1 } },
    ...
  ] }

Une fois le nouveau format stable, j’ai écrit un script à la fin pour le valider et être sûr du résultat. Un schéma vérifie aussi chaque fichier. Les cartes codées en dur (Rumeurs ténébreuses, le tutoriel, les punitions) sont devenues de simples données : des entrées startingDeck et rules.json.

What a pack containsCe que contient un paquet

Backhander runs packs. The original game is one of them (underhand); anyone can write another.

my-pack/
  pack.json         name, version, author, licence, languages, dependencies
  resources.json    the resources (colour, order, symbol, what each can replace)
  cards/*.json      the cards, in as many files as you like
  gods.json         the gods and the cards they unlock
  punishments.json  punishments and their triggers
  chains.json       card chains (event sequences)
  quests.json       side quests
  rules.json        starting hand, blessings, tutorial, layout
  i18n/en.json …    texts, one file per language
  art/  sounds/     images and sounds

Only pack.json is required. Everything is JSON checked against a schema, with no scripts, so a pack cannot run code. A pack can depend on another and add cards to its gods. Every text goes through the translation files, so a pack is translated by adding a file.

Write your own pack: modding Demo, soon

Backhander fait tourner des paquets. Le jeu d’origine en est un (underhand) ; chacun peut en écrire un autre.

mon-paquet/
  pack.json         nom, version, auteur, licence, langues, dépendances
  resources.json    les ressources (couleur, ordre, symbole, ce que chacune remplace)
  cards/*.json      les cartes, en autant de fichiers qu’on veut
  gods.json         les dieux et les cartes qu’ils débloquent
  punishments.json  les punitions et leurs déclencheurs
  chains.json       les chaînes de cartes (suites d’événements)
  quests.json       les quêtes annexes
  rules.json        main de départ, bénédictions, tutoriel, mise en page
  i18n/fr.json …    les textes, un fichier par langue
  art/  sounds/     images et sons

Seul pack.json est obligatoire. Tout est en JSON vérifié par un schéma, sans script, si bien qu’un paquet ne peut pas exécuter de code. Un paquet peut dépendre d’un autre et ajouter des cartes à ses dieux. Chaque texte passe par les fichiers de traduction : traduire un paquet, c’est ajouter un fichier.

Écrire son paquet : le modding Démo, bientôt jouable

About the artÀ propos de l’art

I’m hopeless at drawing. And I didn’t want to reuse the original art, which belongs to its authors.

So I made do. The interface (cards, frames, buttons, altar, symbols) is drawn by the engine, with a bit of SVG. In 2025, during my convalescence, I redrew every card in my own style, a bit more “cute horror” than the original: SVG sketches that I refine with image tools. It takes time, but it keeps me busy. It’s a patchwork and it shows, and I’m happy for someone who can draw to take over. I’m neither a graphic artist nor a designer.

If you can draw and the game speaks to you, you can change all of it. A pack brings its own cards, gods, resource symbols, sounds and texts, so mine can be replaced picture by picture, or the whole game by another. The interface can be changed too: its look (fonts, frames, backgrounds) is a separate theme folder in the project that you can replace without touching the code, and you can even build an interface of your own. In the final version, the theme is a pack too.

Je suis une quiche en dessin. Et je ne voulais pas reprendre l’art d’origine, qui appartient à ses auteurs.

Alors j’ai bricolé. L’interface (cartes, cadres, boutons, autel, symboles) est dessinée par le moteur, avec un peu de SVG. En 2025, pendant ma convalescence, j’ai redessiné toutes les cartes dans mon style, un peu plus « mignonne horreur » que l’original : des croquis en SVG que j’affine avec des outils d’image. Ça prend du temps, mais ça m’occupe. C’est un patchwork et ça se voit, et je serai ravi que quelqu’un qui sait dessiner prenne le relais. Je ne suis ni graphiste ni designer.

Si vous savez dessiner et que le jeu vous parle, vous pouvez tout changer. Un paquet apporte ses propres cartes, dieux, symboles de ressources, sons et textes : on peut remplacer les miens image par image, ou le jeu entier par un autre. L’interface peut aussi changer : son apparence (polices, cadres, fonds) est un dossier de thème à part dans le projet, qu’on peut remplacer sans toucher au code, et on peut même fabriquer sa propre interface. Dans la version finale, le thème est un paquet lui aussi.

TimelineChronologie

  • 2022 I start tinkering with Underhand, with the earliest versions of Godot 4.0, which are still rough. I begin with the fan of cards in your hand and look at how Hearthstone does it. Mine is ugly. Very ugly. Then I turn to SVG. What really grabs me at the time is how the count works between relics, prisoners and cultists. Je commence à triturer Underhand, avec les toutes premières versions de Godot 4.0, encore brutes. Je commence par l’éventail de cartes en main et je regarde comment fait Hearthstone. Le mien est moche. Très moche. Puis je me tourne vers le SVG. Ce qui m’accroche vraiment à l’époque, c’est le fonctionnement du décompte entre reliques, prisonniers et cultistes.
  • 2023 I look inside the APK for the first time, with JD-GUI. I also start to find a way of drawing that is more or less usable for someone whose hands do not respond properly. The result is ugly, but at least I would not have to take the original art, which belongs to its authors. That is when I tell myself I will have to build something pluggable. Je regarde pour la première fois l’intérieur de l’APK, avec JD-GUI. Je commence aussi à trouver une façon de dessiner qui soit plus ou moins utilisable pour quelqu’un dont les mains ne répondent pas correctement. Le résultat est moche, mais au moins je n’aurais pas à récupérer l’art d’origine, qui appartient à ses auteurs. C’est là que je me dis qu’il faudra faire quelque chose d’enfichable.
  • 2024 A first version of the game without the interface, only the rules, in Python, then refined with Ghidra. After that, the port to Godot, and the move to 4.2. Things break, the 3D view of the cards above all. In October, the first 1:1 port of the game, after decompiling it. Une première version du jeu sans l’interface, juste les règles, en Python, puis peaufinée avec Ghidra. Ensuite, le portage sous Godot, et le passage à la 4.2. Des choses cassent, la vue 3D des cartes surtout. En octobre, premier portage 1:1 du jeu, après décompilation.
  • 2025 Brain surgery. A long year of break. During my convalescence, I redraw the cards. It takes time, but it keeps me busy. Brain surgery. Une longue année de pause. Pendant ma convalescence, je redessine les cartes. Ça prend du temps, mais ça m’occupe.
  • 2026 Progressive translation, a proper naming scheme, and a beta version. Backhander takes its shape: packs, chains, quests, the Collection, the 3D card, the choice of your starting hand. Traduction progressive, formalisation du nommage, et une version bêta. Backhander prend sa forme : paquets, chaînes, quêtes, collection, carte en 3D, choix de la main de départ.