De l'usage de Python pour bidouiller les jeux HTML gratuits

J’ai trouvé un jeu, qui rappelle un peu les vieux jeux de stratégie gratuits en ligne tout en HTML qui ont bercés mon enfance, genre Hyperiums (qui se souvient ?).

Capture d’écran d’Hyperiums pour rafraîchir votre mémoire :
Capture d'écran d'Hyperiums

C’est du tour par tour (un tour par an) ça rappelle la lenteur d’Hyperiums :heart: et l’interface n’est pas vraiment meilleure qu’Hyperiums (voire pire, soyons honnêtes, en plus c’est codé en Java). Et y’a MASSSSS joueurs, plus de 10 millions il paraît, mais on peut pas voir, j’imagine la facture de l’infra derrière :smiley:

Là où ils ont mis un réel effort par rapport aux autres jeux HTML c’est sur le moteur de règles.

Les règles sont d’une complexité folle, personne n’a jamais encore réussi à toutes les comprendre, alors chaque joueur se spe dans un domaine, un peu comme dans les meuporgs.

Les règles sont tellement compliquées qu’ils ont inventé un langage dédié (mais pas Turing complet, sniff) pour décrire les règles : le langage M.

Et elles changent subtilement à chaque tour de jeu.

Au total c’était plus de 62k lignes de règles il y a 8 ans, peut-être près de 90_000 aujourd’hui.

Comme à chaque fois que je joue à un vidéo de ce genre, j’essaye de voir si en scriptant un peu avec Python je peux pas bidouiller des trucs.

Et là je cherche, et BOOM, y’a une team française qui joue aussi et qui a rendu open-source une ré-implémentation from scratch du jeu de règles en Python.

Avec on peut simuler un tour du jeu (ou plusieurs) à l’avance pour chercher des opti perchées, faire varier des valeurs, voir comment ça fait réagir le jeu, ou carrément aller lire le code d’un calcul en particulier : vu que c’est du Python, ça se lit comme un livre ouvert.

Bon assez de blah blah, je vous montre le code pour une simulation toute simple, genre Alice et Bob, d’abord il faut déclarer les paramètres de la simulation avec un simple dict :


TEST_CASE = {
    "individus": {
        "Alice": {
            "salaire_imposable": {"2023": 34_567},
        },
        "Bob": {
            "salaire_imposable": {"2023": 23_456},
        },
        "Eve": {}
    },
    "menages": {
        "menage_1": {
            "personne_de_reference": ["Alice"],
            "conjoint": ["Bob"],
            "enfants": ["Eve"],
        }
    },
    "familles": {
        "famille_1": {
            "parents": ["Alice", "Bob"],
            "enfants": ["Eve"],
        }
    },
    "foyers_fiscaux": {
        "foyer_fiscal_1": {
            "declarants": ["Alice", "Bob"],
            "personnes_a_charge": ["Eve"],
            "f7uf": {"2023": 860},  # Dons à Framasoft
            "f7ea": {"2023": 1},  # Eve est au collège
        },
    },
}

Ensuite on charge ça dans le simulateur de règles :

tax_benefit_system = CountryTaxBenefitSystem()

simulation_builder = SimulationBuilder()
sim = simulation_builder.build_from_entities(tax_benefit_system, TEST_CASE)

et BOOM on peut lui faire calculer n’importe quelle variable :

print(sim.calculate("impot_revenu_restant_a_payer", "2023"))

OK j’ai pris quelques raccourcis (les imports, tout ça). Voici un script qui fait un joli résumé avec rich de toutes les variables importantes du point de vue d’Alice et Bob, copie-collé ça marche :

(bon euh, pip install git+https://github.com/openfisca/openfisca-france rich avant hin)

from openfisca_core.simulation_builder import SimulationBuilder
from openfisca_france import CountryTaxBenefitSystem
from openfisca_core.model_api import ADD
import warnings
import sys
from rich.console import Console
import re

console = Console()

warnings.simplefilter("ignore")


TEST_CASE = {
    "individus": {
        "Alice": {
            "salaire_imposable": {"2023": 34_567},
        },
        "Bob": {
            "salaire_imposable": {"2023": 23_456},
        },
        "Eve": {}
    },
    "menages": {
        "menage_1": {
            "personne_de_reference": ["Alice"],
            "conjoint": ["Bob"],
            "enfants": ["Eve"],
        }
    },
    "familles": {
        "famille_1": {
            "parents": ["Alice", "Bob"],
            "enfants": ["Eve"],
        }
    },
    "foyers_fiscaux": {
        "foyer_fiscal_1": {
            "declarants": ["Alice", "Bob"],
            "personnes_a_charge": ["Eve"],
            "f7uf": {"2023": 860},  # Dons à Framasoft
            "f7ea": {"2023": 1},  # Eve est au collège
        },
    },
}

avance_perçue_sur_les_réductions_et_crédits_d_impôt = {"2023": 99}
prelevement_a_la_source = {"2023": 200}

tax_benefit_system = CountryTaxBenefitSystem()

simulation_builder = SimulationBuilder()
sim = simulation_builder.build_from_entities(tax_benefit_system, TEST_CASE)


class SimulationStudy:
    def __init__(self, simulation, year):
        self.simulation = simulation
        self.year = year

    def calc(self, label):
        return self.simulation.calculate(label, self.year)

    def print(self, indent, label, help=None):
        value = self.calc(label)
        if indent:
            console.print("    " * indent, end="")
        if "taux_" in label:
            console.print(f"{label} = [bold]{value.sum():.1%}[/bold]", end="")
        else:
            console.print(f"{label} = [bold]{round(value.sum())}[/bold]", end="")
        if help:
            console.print(f" ([italic]{help}[/italic])", end="")
        print()


def parse_args():
    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument("-v", "--verbose", action="store_true")
    parser.add_argument("-y", "--years", nargs="*", default=("2023",))
    return parser.parse_args()


do_not_show = (
    "plafond_securite_sociale",
    "contrat_de_travail",
    "nombre_jours_calendaires",
    "quotite_de_travail",
    " age<",
    "zone_apl",
    "apprentissage_contrat",
    "titre_restaurant_taux_employeur",
    "date_naissance",
    "nbptr",
)


def main():
    args = parse_args()
    for year in args.years:
        study = SimulationStudy(sim, year)
        console.rule(f"[bold red]Revenus {int(year) - 1} déclarés en {year}")

        if args.verbose:
            sim.trace = True
            study.calc("impot_revenu_restant_a_payer")
            for line in sim.tracer.computation_log.lines():
                if re.search(">>.*[1-9]", line):
                    if any(pat in line for pat in do_not_show):
                        continue
                    print(line)
            break

        study.print(0, "nbptr", "Nombre de parts")
        study.print(0, "rbg", "Revenu brut global")
        study.print(1, "revenu_categoriel")
        study.print(2, "revenu_categoriel_deductions")
        study.print(3, "traitements_salaires_pensions_rentes")
        study.print(4, "revenu_assimile_salaire")
        study.print(4, "revenu_assimile_salaire_apres_abattements")
        study.print(4, "abattement_salaires_pensions")
        study.print(3, "indu_plaf_abat_pen")
        study.print(2, "revenu_categoriel_capital")
        study.print(2, "revenu_categoriel_foncier")
        study.print(2, "revenu_categoriel_non_salarial")
        study.print(2, "revenu_categoriel_plus_values")

        study.print(
            0, "rng", "Revenu net global (rbg - csg_patrimoine - charges_deduc)"
        )
        study.print(1, "csg_patrimoine_deductible_ir")
        study.print(1, "charges_deduc")

        study.print(0, "rni", "Revenu net imposable (rng - abat_spe)")
        study.print(1, "abat_spe", "Abattements spéciaux")

        console.rule("[red]QUOTIENT FAMILIAL")
        study.print(0, "ir_plaf_qf", "Impôt après plafonnement quotient familial")
        study.print(1, "ir_ss_qf", "Impôt sans quotient familial")
        study.print(1, "avantage_qf")
        study.print(1, "ir_brut", "Impôt sur les revenus soumis au barème")
        study.print(1, "ir_tranche")
        study.print(1, "ir_taux_marginal")

        console.rule("[red]IMPÔT NET")
        study.print(0, "ip_net", "Impôt net avant réductions (ir_plaf_qf - decote)")
        study.print(1, "decote")
        study.print(2, "decote_gain_fiscal")

        console.rule("[red]RÉDUCTIONS D'IMPÔT")
        study.print(0, "reductions")
        study.print(1, "reductions_plafonnees")
        study.print(1, "reductions_deplafonnees")
        study.print(2, "dfppce", "Dons à des organismes d'intérêt général")
        study.print(3, "f7uf")
        study.print(2, "reduction_enfants_scolarises")
        study.print(0, "iaidrdi", "Impôt après imputation des réductions")
        study.print(0, "iai", "Impôt avant imputations de l'impôt sur le revenu")

        console.rule("[red]TOTAL DE L'IMPOSITION")
        study.print(0, "impot_revenu_restant_a_payer")
        console.print(
            "    [i]iai - credits_impot - accomptes_ir - prelevements_forfaitaires + contribution_hauts_revenus[/i]"
        )
        study.print(1, "contribution_exceptionnelle_hauts_revenus")
        study.print(1, "prelevement_forfaitaire_unique_ir")
        study.print(1, "prelevement_forfaitaire_liberatoire")

        console.rule("[red]PRÉLÈVEMENT À LA SOURCE ET AVANCE PERÇUE")
        avance_percue = avance_perçue_sur_les_réductions_et_crédits_d_impôt[year]
        console.print(f"avance_percue = [bold]{avance_percue}[/bold]")
        pas = prelevement_a_la_source[year]
        console.print(f"preleve_a_la_source = [bold]{pas}[/bold]")

        console.rule("[red]RESTE À PAYER")
        console.print(
            "[i]    -impot_revenu_restant_a_payer - preleve_a_la_source + avance_percue[/i]\n"
            "[i]    car par convention impot_revenu_restant_a_payer est négatif[/i]"
        )
        console.print(
            f"  [b]{round(-sim.calculate('impot_revenu_restant_a_payer', year)[0] - pas + avance_percue)}[/b]\n"
        )
        study.print(0, "taux_moyen_imposition")

        console.rule("[red]REVENU FISCAL")
        study.print(0, "rfr", "Revenu fiscal de référence")
        study.print(1, "rni", "Revenu net imposable")


if __name__ == "__main__":
    main()

Le résultat :

Qui est chaud pour qu’on se monte une équipe ?

2 « J'aime »

HAHA épique y’a même des boucliers, #NEED, vive grep :

openfisca_france/model/prelevements_obligatoires/isf.py:class bouclier_imp_gen(Variable):  # # ajouter CSG- CRDS

j’vais m’remonter un paladin #souvenirs.

1 « J'aime »

Et en fait dans 1 an, tu va découvrir que le jeu a été fait par Bercy en s’inspirant de la Stratégie Ender (mais je vais pas en dire plus pour ne pas spoiler un livre vieux de 30 ans)

T’es un grand malade toi, j’ai bien mis 5 minutes à comprendre…

1 « J'aime »