Production UZDoom autonome

RED FLAGS 2: COLOR OF POMEGRANADE

L’autonomie est une propriété d’ingénierie, pas un slogan : le paquet porte sa propre identité, ses polices, sa palette, ses sons et sa géométrie, et une suite de tests fait échouer le build si un seul lump commercial s’y glisse.

ZScriptUZDoom 4.14.3 (unmodified)PythonUDMFpytestHeadless capture harnessSHA-256 manifests
Statut
En production / vertical slice physiquement traversée, art lock ouvert
Code
RF2-17
Domaine
Production UZDoom autonome
Porté par
Pomegranate Interactive
Moteur
UZDoom 4.14.3 · non modifié
Nom de code
POMEGRENADE (interne, historique)
Distribution
Aucune · non publié

Un jeu de tir à la première personne autonome, construit comme un paquet sur moteur non modifié n’exigeant aucune donnée de jeu commerciale : une carte de production est intégrée, prouvée à l’exécution et traversée à pied du menu principal jusqu’à l’interrupteur de sortie, tandis que vingt-deux autres cartes, huit autres armes et l’art lock du propriétaire restent devant.

1 153Entrées déterministes dans le paquet
233Secteurs dans la carte de production intégrée
58 / 58Suite statique, telle que rapportée par le projet
1 sur 23Cartes intégrées

Profil de mission

  • Livrer un paquet autonome n’exigeant aucune donnée de jeu commerciale, vérifié par des tests plutôt qu’affirmé dans un readme.
  • Écrire chaque carte comme un contrat indépendant du moteur, compilé en géométrie, pour que le design survive à une décision de moteur.
  • Modéliser les armes comme des graphes d’états explicites où maniement, recul, rechargement et sélecteur sont des images animées écrites, non des constantes de réglage.
  • Lier chaque pack d’art importé à un manifeste et prouver l’identité octet à octet à travers l’empaquetage.
  • Séparer ce qu’une machine a prouvé de ce qu’une personne a physiquement fait, et laisser le verdict esthétique du propriétaire non signé jusqu’à ce qu’il le signe.

Architecture

État vérifié

Ce qui existe et a été exercé au moment de la rédaction.

  • Le moteur est un build stable non modifié, et le paquet est une archive autonome de 1 153 entrées déterministes n’exigeant aucune donnée de jeu commerciale.
  • Le build est reproductible par construction : les entrées sont triées, les horodatages fixés, et un manifeste consigne un SHA-256 par fichier à côté du hash du paquet.
  • Des tests statiques tiennent le paquet à ses contrats, dont une gate qui fait échouer le build sur tout nom de lump commercial et une autre sur tout préfixe de sprite commercial.
  • Une carte de production est intégrée : 233 secteurs, 978 lignes, 67 lumières dynamiques, 2 018 cellules atteignables, 2 secrets et 47 placements de props de production non interactifs.
  • Quatre armes sont implémentées comme graphes d’états écrits — un pistolet à 13 coups, un fusil à 20 coups doté d’un sélecteur semi/automatique, un projectile à quatre bandes de souffle et une arme de mêlée.
  • Le protagoniste porte santé, armure séparée, cinq états de portrait et un état de rage, et la sélection de portrait est une fonction pure de cet état, aux seuils repris du contrat de moteur antérieur.
  • Le trajet du menu principal jusqu’à l’interrupteur de sortie a été accompli à pied avec de vraies entrées, y compris les deux clés, chaque porte obligatoire, les deux portails distants, une sauvegarde et un rechargement dans un processus neuf distinct.
  • Les packs d’art importés sont octet-identiques à travers l’empaquetage : le pack d’ennemis livre 148 images de production et le pack de props de la carte 369 rotations de sprites, chacun prouvé contre son manifeste par test.

Limites assumées

Ce que ce projet n’est pas, écrit ici pour que personne n’ait à le découvrir plus tard.

  • Ce n’est pas publié. Il n’y a ni page de distribution, ni bundle de plateforme, ni build public ; le paquet existe sur la machine qui le construit.
  • Une carte sur vingt-trois est intégrée. Les vingt-deux autres sont des contrats écrits, non du contenu jouable, et la production de la deuxième n’a pas commencé.
  • Quatre armes sur douze existent. Les huit autres n’ont pas été commencées.
  • L’art lock du propriétaire est ouvert. Les images d’armes montrent encore des bords réparés, une livraison requise pour l’état « fusil vide » manque, et le pack d’ennemis a passé la revue technique tandis que son flux de transparence attend un verdict esthétique.
  • L’audio est une banque de remplissage synthétisée déterministe. La musique n’a pas commencé.
  • Le trajet automatisé prouve une complétude technique. Il ne prouve ni que le jeu est amusant, ni que sa difficulté est juste, ni que quelqu’un d’autre que son auteur y a joué.
  • Les chiffres de cette page sont rapportés depuis les documents de statut du projet et son dépôt à un commit nommé. Ce build n’a rejoué aucune gate.

Le nom, et le nom de code

Le titre est RED FLAGS 2: COLOR OF POMEGRANADE. C’est la chaîne inscrite dans l’identité de démarrage du paquet et dans sa table de localisation, et c’est ce qu’emploient les documents de statut du dépôt. POMEGRENADE n’est pas le titre : c’est l’arme de jet, le nom de la palette, et le nom de travail interne et historique que le projet a porté avant que le titre ne se fixe. Cette page tient la distinction, parce qu’un nom de code discrètement promu en titre est le plus petit mensonge possible et le plus facile à éviter.

Le projet a commencé sur un autre moteur puis a migré vers l’actuel, ce qui a supprimé les deux contraintes rendant la ligne antérieure non livrable : une restriction d’usage commercial, et une dépendance à un fichier de données commercial non redistribuable. L’implémentation archivée est préservée sans modification sous un tag. Le produit final ne dépend d’aucune donnée de jeu commerciale.

Un paquet déterministe, ou pas de paquet

Le build parcourt l’arbre de jeu dans l’ordre trié, écrit chaque entrée avec un horodatage fixe, et consigne un SHA-256 par fichier dans un manifeste à côté du hash du paquet. Deux builds du même arbre produisent donc les mêmes octets, ce qui est la seule raison pour laquelle un hash de paquet mérite d’être nommé dans un document de statut. Les lumps requis sont contrôlés par nom exact, et une police rangée sous un chemin organisé n’a explicitement pas le droit de satisfaire le nom nu que le moteur recherche en interne.

CODE DU DÉPÔTtools/build_ipk3.py:245-276@6a890edc5be5Python
def assemble(out_path):
    entries = []
    for dirpath, dirnames, filenames in os.walk(GAME):
        dirnames.sort()
        for fn in sorted(filenames):
            full = os.path.join(dirpath, fn)
            rel = os.path.relpath(full, GAME).replace(os.sep, "/")
            entries.append((rel, full))
    entries.sort()
    # A FON2 stored only as fonts/BIGFONT is addressable by the explicit path
    # "fonts/BIGFONT", but it does not replace the engine's already-registered
    # bare "BigFont". That leaks the stock chunky face into protected menus and
    # any menu descriptor that the engine constructs internally. Keep the
    # organised copies for explicit lookups and add byte-identical root aliases
    # so the game's fonts win every bare-name lookup as well.
    for name in ROOT_FONT_ALIASES:
        source = os.path.join(GAME, "fonts", name)
        if os.path.isfile(source) and not any(rel == name for rel, _full in entries):
            entries.append((name, source))
    entries.sort()
    manifest = []
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
        for rel, full in entries:
            with open(full, "rb") as fh:
                data = fh.read()
            info = zipfile.ZipInfo(rel, date_time=FIXED_DATE)
            info.compress_type = zipfile.ZIP_DEFLATED
            info.external_attr = 0o644 << 16
            z.writestr(info, data)
            manifest.append({"path": rel, "size": len(data), "sha256": sha256_bytes(data)})
    return manifest

Le commentaire est un rapport de bug devenu règle. Une police présente seulement sous un chemin organisé laissait les menus propres au moteur s’afficher avec sa fonte d’origine : des alias racine octet-identiques sont donc ajoutés délibérément, et le contrôle de lumps requis refuse d’accepter la copie organisée à leur place.

Rien de commercial n’entre dans le paquet

L’autonomie est affirmée par un test, non par une phrase. Une gate refuse tout fichier dont le nom de base correspond à un motif de données commerciales. Une autre refuse tout sprite dont le préfixe de quatre lettres entre en collision avec le jeu de sprites ancestral du moteur, et refuse en outre tout sprite qui ne suit pas la grammaire de nommage du projet. D’autres gates vérifient que les définitions de cartes, la table de localisation et les définitions de menus n’exposent aucun de ces noms au joueur.

CODE DU DÉPÔTtests/test_package_static.py:556-568@6a890edc5be5Python
def test_no_external_iwad_or_commercial_data(pkg):
    _, names = pkg
    bad = [n for n in names if FORBIDDEN_NAMES.match(os.path.basename(n))]
    assert not bad, bad


def test_no_doom_sprite_names_or_lumps(pkg):
    _, names = pkg
    sprites = {os.path.basename(n)[:4].upper() for n in names if n.startswith("sprites/")}
    assert not (sprites & DOOM_SPRITES), sorted(sprites & DOOM_SPRITES)
    bad = [n for n in names
           if n.startswith("sprites/") and not re.match(r"sprites/[A-Z0-9]{4}[A-Z][0-8]\.png$", n)]
    assert not bad, bad[:10]

Trois assertions, chacune sur le paquet construit et non sur l’arbre source. Le test de sprites est le plus strict : éviter les noms ancestraux ne suffit pas, chaque sprite doit aussi respecter la grammaire de quatre lettres du projet — un import accidentel ne peut donc pas passer au seul motif qu’il est inconnu.

Les armes sont des graphes d’états, pas des tables de réglage

Chaque arme est un graphe explicite d’états nommés aux cadences d’images écrites : prêt, prêt à vide, sélection, désélection, tir, tir automatique, tir à vide, appui et retour du sélecteur, et un rechargement décomposé en chargeur sorti, épaule, chargeur entré, culasse et retour. Les deux modes de tir du fusil ne changent que la cadence et le contrôle — dégâts par cartouche et économie de munitions sont identiques — et cette règle est écrite dans le fichier comme canon, plutôt que laissée à découvrir par une passe d’équilibrage ultérieure.

CODE DU DÉPÔTgame/zscript/weapons/PomFAL.zs:232-249@6a890edc5be5ZScript
    AltFire:
        FALG A 0
        {
            invoker.fullAuto = !invoker.fullAuto;
            // Keep the engine's Ready-state edge latch enabled in both modes.
            // FireAuto performs its own held-trigger loop with A_ReFire, which
            // bypasses NoAutoFire; clearing this flag only made a held selector
            // key enter AltFire twice and undo the requested mode change.
            invoker.bNoAutoFire = true;
            invoker.burstRound = 0;
            A_StartSound("pom/fal/mode", CHAN_WEAPON);
            PomVoice.Tell(StringTable.Localize(
                invoker.fullAuto ? "$POM_FAL_AUTO" : "$POM_FAL_SEMI"));
            PomClearMotion();
            PomBeginMotionStage(4, 0, POM_REST_Y,
                                1.2, POM_REST_Y + 2.5);
            return ResolveState("ModePress");
        }

Le commentaire consigne un défaut et sa cause : le chemin automatique exécute sa propre boucle de gâchette maintenue, qui contourne le verrou de front du moteur ; effacer ce drapeau faisait donc entrer deux fois dans l’état une touche de sélecteur maintenue, annulant le changement de mode qu’elle venait de demander. Le correctif tient en une ligne, et il est expliqué là où il vit.

Le visage du protagoniste est une fonction de l’état

La barre d’état dessine un portrait, la santé, l’armure séparée, un compte numérique pour l’arme de jet, le nom complet de l’arme et un couple chargeur/réserve. Le portrait n’est pas scénarisé par événements : c’est une fonction pure de l’état courant du joueur, évaluée à chaque image, avec un état de rage qui surpasse l’échelle de blessure située en dessous.

CODE DU DÉPÔTgame/zscript/core/PomCore.zs:32-44@6a890edc5be5ZScript
    static clearscope int PortraitState(PlayerPawn pawn)
    {
        // 4=RAGE overrides all, then 3=NEAR-DEATH <11, 2=CRITICAL <31,
        // 1=HURT <61, 0=NORMAL. Thresholds carried exactly from the
        // EDuke32 bootstrap contract.
        let pp = PomPlayer(pawn);
        if (pp && pp.pomRageTics > 0) return 4;
        int h = pawn ? pawn.health : 0;
        if (h < 11) return 3;
        if (h < 31) return 2;
        if (h < 61) return 1;
        return 0;
    }

Cinq états, quatre seuils, un dépassement, aucun historique. La sélection étant pure, le HUD ne peut pas se désynchroniser de la simulation, et les seuils sont repris mot pour mot du contrat de moteur antérieur du projet plutôt que réinventés pendant la migration.

Prouvé par machine, prouvé physiquement, accepté humainement

Le projet tient trois vocabulaires distincts et refuse de les confondre. « Implémenté » signifie que le code existe. « Intégré » signifie qu’il est dans le paquet autonome. « Prouvé à l’exécution » signifie que le moteur l’a exercé. « Prouvé physiquement » signifie que de vraies entrées clavier et souris l’ont atteint sans triche ni commande console modifiant l’état. Aucun de ces termes ne signifie accepté par le propriétaire, et aucun ne signifie art lock.

Suite statique
58 sur 58, telle que rapportée par le document de statut du projet.
Slice d’exécution
77 sur 77, rapporté.
Gate de contrôles
10 sur 10, rapporté.
Entrées physiques
8 sur 8, rapporté.
Trajet physique
Accepté : 45 segments, 977 échantillons, aucune discontinuité au-delà du seuil d’échantillonnage.
Chargement en processus neuf
Correspondance champ à champ exacte après rechargement dans un processus distinct.
Art lock propriétaire
Ouvert. L’automatisation ne peut pas le signer et cette page ne le suggère pas.