Hackathon Code Code Codeur

En mai 2017, ENI et IcySoft ont organisé un hackathon assez sympa, Code Code Codeur, et je n'ai pas pu m’empêcher d'y participer. 

L'objectif était de développer en quelques jours un algorithme animant un personnage dans un combat en tête à tête. Ce développement se faisait en Javascript qui était ensuite exécuté par leur serveur.

Pour développer, Icysoft avait développé une interface (en Angular) présentant le code qui était directement éditable, l’arène et une trace de debug :

Durant la phase de développement, IcySoft était assez présent, même le week-end : Lorsque je leur soumettait des bugs, ils s'empressaient de les corriger. Cela a parfois abouti à des changements importants comme la modification de la distance de frappe, mais c'était important qu'ils rectifient les différents problèmes.

Définir l'approche!

Dans un premier temps, il a fallu définir l'approche à retenir au niveau de l'algo, mais pour cela il faut tout d'abord comprendre les contraintes de la plateforme. J'ai donc effectué une série de tests avec des actions uniques pour voir le comportement aux limites. J'ai également tenté de mémoriser des états d'un tour à l'autre, mais là c'était sans espoir. Cela avait été sciemment écarté par IcySoft pour éviter des algo trop complexes : C'est une contrainte assez importante car cela interdit toute adaptation sur la base des coups précédents, mais soit, c'était la règle!

Ne pouvant pas mémoriser d'état, et n'étant pas fan de l'aléatoire, je me suis lancé sur un algo assez simple, déterministe, à coup de décisions heuristiques au regard de la situation du moment.

Pour tester, IcySoft mettait en face un combattant au comportement plutôt aléatoire.

Au fil de mes tests, j'ai pu affiner mon algo en réaction à leur combattant aléatoire, malheureusement, au bout de 2/3 jours, j'ai compris que le combattant aléatoire était un boulet qui ne permettait pas de tout tester : Pratiquement impossible de tester la mode défensif, impossible de tester aussi certains modes de fonctionnement qu'un autre assaillant pourrait adopter (que de la parade, pas d'attaque directe, ...).

Pendant un moment, j'ai initié le développement d'un mini simulateur sous NodeJS qui me permettrait de mettre un autre combattant en face pour mes tests, mais je n'ai pas pu aboutir.

Au final, j'étais assez content de mon programme ci-dessous et j'attendais avec impatience le jour des matchs.

Mon code

/**
 * Created by emmguyot on 17/05/2017.
 */
// Liste des actions disponibles
const actions = [
    action.run,
    action.walk,
    action.back,
    action.highGuard,
    action.lowGuard,
    action.attack,
    action.jumpAttack,
    action.pull
];

// Liste des parades
const secure = [
    action.back,
    action.highGuard,
    action.lowGuard
];

// Liste des attaques
const offensive = [
    action.run,
    action.attack,
    action.jumpAttack,
    action.pull
];

function nearMyEdge(limit) {
    return ((me.direction === "east")
                && (me.position <= limit))
        || ((me.direction !== "east")
                && (me.position >= (battle.ringWidth - limit)));
}

function opponentNearHisEdge(limit) {
    return ((opponent.direction === "east")
                && (opponent.position <= limit))
        || ((opponent.direction !== "east")
                && (opponent.position >= (battle.ringWidth - limit)));
}

function opponentHasToWait2Round() {
    return (opponent.currentAction === "pull")
        || (opponent.currentAction === "jumpAttack");
}

function opponentHasToWait1Round() {
    return (opponent.currentAction === "pull")
        || (opponent.currentAction === "jumpAttack")
        || (opponent.currentAction === "attack")
        || (opponent.currentAction === "cooldown_2")
        // au cas où il recul (peu probable car dans ce cas,
        // je serai en cooldown et donc pas d'action
        || (opponent.cooldown === 1)
        ;
}

// Récupérer la distance entre l'adversaire et l'IA
const dist = Math.abs(me.position - opponent.position);
const ecartVie = me.life - opponent.life;

function strategieNormale() {
    if (dist > 8) {
        action.run();
    }
    else if (dist > 6) { // 7 - 8
        if ((me.currentAction === "recoil") && !nearMyEdge(battle.ringWidth / 2)) {
            // Recule encore pour permettre les futures attaques plus librement
            action.back();
        }
        else {
            if (opponentHasToWait2Round()) {
                // Cours pour attaquer ensuite (à distance)
                action.run();
            }
            else {
                action.walk();
            }
        }
    }
    else if (dist > 5) { // = 6 > Position clef
        // Position neutre > Attente, mais pas trop...
        if (opponentHasToWait2Round()) {
            if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
                // Cours pour attaquer ensuite
                action.run();
            }
            else {
                // Avance prudemment
                action.walk();
            }
        }
        else {
            // Temporise en se protegeant (mais ne sert pas à grand chose)
            action.lowGuard();
        }
    }
    else if (dist > 3) { // 4 - 5
        if (opponentHasToWait2Round()) {
            if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
                action.run();
            }
            else {
                action.pull();
            }
        }
        else {
            if (ecartVie < 0) {
                action.back();
            }
            else {
                action.pull();
            }
        }
    }
    else if (dist > 1) { // 2 - 3
        if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
            action.jumpAttack();
        }
        else {
            if (ecartVie < 0) {
                action.back();
            }
            else {
                action.pull();
            }
        }
    }
    else { // 1
        // A portée, donc attaque systématique.
        // Sinon, c'est cadeau pour l'autre qui ne manquera pas de le faire
        action.attack();
    }
}

function strategieDefensive() {
    if (opponentHasToWait2Round()) {
        if ((dist <= 5) && (dist > 3)) {
            action.pull();
        }
        else if ((dist <= 3) && (dist > 1)) {
            action.jumpAttack();
        }
        else if (dist <= 1) {
            action.attack();
        }
    }
    else {
        if (dist > 5) {
            action.walk();
        }
        else if (dist > 3) {
            action.lowGuard();
        }
        else if (dist > 1) {
            action.highGuard();
        }
        else {
            action.lowGuard();
        }
    }
}

function strategieOffensive() {
    if (dist > 8) {
        action.run();
    }
    else if (dist > 5) {
        action.run();
    }
    else if (dist > 1) { // 2 - 5
        if (opponentHasToWait1Round()) {
            if (dist >= 4) { // 4-5
                action.run();
            }
            else if (!opponentNearHisEdge(2)) {
                action.jumpAttack();
            }
            else {
                action.run();/**/
            }
        }
        else if (dist > 3) {
            // Attaque aléatoire seulement si besoin de faire la différence
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.lowGuard();
            }
            else {
                action.pull();
            }
        }
        else {
            // Attaque aléatoire seulement si besoin de faire la différence
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.highGuard();
            }
            else {
                action.jumpAttack();
            }
        }
    }
    else { // 1
        if (opponentHasToWait2Round() || opponentHasToWait1Round()) {
            action.attack();
        }
        else {
            // Il peut frapper : Protection
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.lowGuard();
            }
            else {
                action.attack();
            }
        }
    }
}

// Main
if (nearMyEdge(1)) {
    // Il faut se dégager
    strategieDefensive();
}
else if (battle.tickLeft > 200) {
    strategieOffensive();
}
else {
    strategieNormale();
}


// Liste des actions disponibles
const actions = [
    action.run,
    action.walk,
    action.back,
    action.highGuard,
    action.lowGuard,
    action.attack,
    action.jumpAttack,
    action.pull
];

// Liste des parades
const secure = [
    action.back,
    action.highGuard,
    action.lowGuard
];

// Liste des attaques
const offensive = [
    action.run,
    action.attack,
    action.jumpAttack,
    action.pull
];

function nearMyEdge(limit) {
    return ((me.direction === "east")
                && (me.position <= limit))
        || ((me.direction !== "east")
                && (me.position >= (battle.ringWidth - limit)));
}

function opponentNearHisEdge(limit) {
    return ((opponent.direction === "east")
                && (opponent.position <= limit))
        || ((opponent.direction !== "east")
                && (opponent.position >= (battle.ringWidth - limit)));
}

function opponentHasToWait2Round() {
    return (opponent.currentAction === "pull")
        || (opponent.currentAction === "jumpAttack");
}

function opponentHasToWait1Round() {
    return (opponent.currentAction === "pull")
        || (opponent.currentAction === "jumpAttack")
        || (opponent.currentAction === "attack")
        || (opponent.currentAction === "cooldown_2")
        // au cas où il recul (peu probable car dans ce cas,
        // je serai en cooldown et donc pas d'action
        || (opponent.cooldown === 1)
        ;
}

// Récupérer la distance entre l'adversaire et l'IA
const dist = Math.abs(me.position - opponent.position);
const ecartVie = me.life - opponent.life;

function strategieNormale() {
    if (dist > 8) {
        action.run();
    }
    else if (dist > 6) { // 7 - 8
        if ((me.currentAction === "recoil") && !nearMyEdge(battle.ringWidth / 2)) {
            // Recule encore pour permettre les futures attaques plus librement
            action.back();
        }
        else {
            if (opponentHasToWait2Round()) {
                // Cours pour attaquer ensuite (à distance)
                action.run();
            }
            else {
                action.walk();
            }
        }
    }
    else if (dist > 5) { // = 6 > Position clef
        // Position neutre > Attente, mais pas trop...
        if (opponentHasToWait2Round()) {
            if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
                // Cours pour attaquer ensuite
                action.run();
            }
            else {
                // Avance prudemment
                action.walk();
            }
        }
        else {
            // Temporise en se protegeant (mais ne sert pas à grand chose)
            action.lowGuard();
        }
    }
    else if (dist > 3) { // 4 - 5
        if (opponentHasToWait2Round()) {
            if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
                action.run();
            }
            else {
                action.pull();
            }
        }
        else {
            if (ecartVie < 0) {
                action.back();
            }
            else {
                action.pull();
            }
        }
    }
    else if (dist > 1) { // 2 - 3
        if (!opponentNearHisEdge(2) || (ecartVie > 10)) {
            action.jumpAttack();
        }
        else {
            if (ecartVie < 0) {
                action.back();
            }
            else {
                action.pull();
            }
        }
    }
    else { // 1
        // A portée, donc attaque systématique.
        // Sinon, c'est cadeau pour l'autre qui ne manquera pas de le faire
        action.attack();
    }
}

function strategieDefensive() {
    if (opponentHasToWait2Round()) {
        if ((dist <= 5) && (dist > 3)) {
            action.pull();
        }
        else if ((dist <= 3) && (dist > 1)) {
            action.jumpAttack();
        }
        else if (dist <= 1) {
            action.attack();
        }
    }
    else {
        if (dist > 5) {
            action.walk();
        }
        else if (dist > 3) {
            action.lowGuard();
        }
        else if (dist > 1) {
            action.highGuard();
        }
        else {
            action.lowGuard();
        }
    }
}

function strategieOffensive() {
    if (dist > 8) {
        action.run();
    }
    else if (dist > 5) {
        action.run();
    }
    else if (dist > 1) { // 2 - 5
        if (opponentHasToWait1Round()) {
            if (dist >= 4) { // 4-5
                action.run();
            }
            else if (!opponentNearHisEdge(2)) {
                action.jumpAttack();
            }
            else {
                action.run();/**/
            }
        }
        else if (dist > 3) {
            // Attaque aléatoire seulement si besoin de faire la différence
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.lowGuard();
            }
            else {
                action.pull();
            }
        }
        else {
            // Attaque aléatoire seulement si besoin de faire la différence
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.highGuard();
            }
            else {
                action.jumpAttack();
            }
        }
    }
    else { // 1
        if (opponentHasToWait2Round() || opponentHasToWait1Round()) {
            action.attack();
        }
        else {
            // Il peut frapper : Protection
            if ((Math.random() > 0.5) || (ecartVie > 6)) {
                action.lowGuard();
            }
            else {
                action.attack();
            }
        }
    }
}

// Main
if (nearMyEdge(1)) {
    // Il faut se dégager
    strategieDefensive();
}
else if (battle.tickLeft > 200) {
    strategieOffensive();
}
else {
    strategieNormale();
}

Le jour J

Ça y est, c'est le jour de la bataille tant attendue. La formule retenue par ENI et IcySoft était une formule par élimination pour départager les 618 participants : A la première défaite, tout est fini. Alors ça va être tendu...

1er tour

Le premier tour s'enclenche et ça se passe plutôt bien pour moi.

2nd Tour

C'est beaucoup plus serré! Je commence à me poser des questions

3ème tour

Ça part pas mal, mais je suis à la traîne... Puis gros bug!! Je n'attaque plus et lui non plus. La fin n'est pas très passionnante, à mon grand désespoir!

J'avais bien un peu anticipé le cas où l'adversaire n'attaquerait pas trop. C'est ce qui m'avais amené à créer une phase particulièrement offensive en début de partie, mais à ce stade, dans une situation de défaite, c'est tout sauf suffisant. Bref, je suis déçu et je m'arrête donc à ce 3ème tour sur les 10 de la compétition.

Rétrospective de la soirée

Afin d'avoir une vision de toute la soirée avec de nombreux autres combats, regardez la vidéo sur de Facebook.