<commit_before>import {Craft} from '../../../model/garland-tools/craft';
import {CraftingAction} from '../model/actions/crafting-action';
import {ActionResult} from '../model/action-result';
import {CrafterStats} from '../model/crafter-stats';
import {EffectiveBuff} from '../model/effective-buff';
import {Buff} from '../model/buff.enum';
import {SimulationResult} from './simulation-result';
import {SimulationReliabilityReport} from './simulation-reliability-report';

export class Simulation {

    public progression = 0;
    public quality = 0;
    public startingQuality = 0;
    // Durability of the craft
    public durability: number;

    public state: 'NORMAL' | 'EXCELLENT' | 'GOOD' | 'POOR';

    public availableCP: number;
    public maxCP: number;

    public buffs: EffectiveBuff[] = [];

    public success: boolean;

    public steps: ActionResult[] = [];

    constructor(public readonly recipe: Craft, public readonly actions: CraftingAction[], private _crafterStats: CrafterStats,
                hqIngredients: { id: number, amount: number }[] = []) {
        this.durability = recipe.durability;
        this.availableCP = this._crafterStats.cp;
        this.maxCP = this.availableCP;
        for (const ingredient of hqIngredients) {
            // Get the ingredient in the recipe
            const ingredientDetails = this.recipe.ingredients.find(i => i.id === ingredient.id);
            // Check that the ingredient in included in the recipe
            if (ingredientDetails !== undefined) {
                this.quality += ingredientDetails.quality * ingredient.amount;
            }
        }
        this.startingQuality = this.quality;
    }

    public getReliabilityReport(): SimulationReliabilityReport {
        const results: SimulationResult[] = [];
        // Let's run the simulation 500 times.
        for (let i = 0; i < 500; i++) {
            results.push(this.run(false));
            this.reset();
        }
        const successPercent = (results.filter(res => res.success).length / results.length) * 100;
        const hqPercent = results.reduce((p, c) => p + c.hqPercent, 0) / results.length;
        let hqMedian = 0;
        results.map(res => res.hqPercent).sort((a, b) => a - b);
        if (results.length % 2) {
            hqMedian = results[results.length / 2].hqPercent;
        } else {
            hqMedian = (results[Math.floor(results.length / 2)].hqPercent + results[Math.ceil(results.length / 2)].hqPercent) / 2;
        }
        return {
            rawData: results,
            successPercent: Math.round(successPercent),
            averageHQPercent: Math.round(hqPercent),
            medianHQPercent: hqMedian
        }
    }

    public getMinStats(): { control: number, craftsmanship: number, cp: number } {
        // Three loops, one per stat
        while (this.run(true).success) {
            this.crafterStats.craftsmanship--;
            this.reset();
        }
        while (this.run(true).success) {
            this.crafterStats._control--;
            this.reset();
        }
        while (this.run(true).success) {
            this.crafterStats.cp--;
            this.reset();
        }
        return {
            control: this.crafterStats.getControl(this),
            craftsmanship: this.crafterStats.craftsmanship,
            cp: this.crafterStats.cp
        };
    }

    public reset(): void {
        delete this.success;
        this.progression = 0;
        this.durability = this.recipe.durability;
        this.quality = this.startingQuality;
        this.buffs = [];
        this.steps = [];
        this.maxCP = this.crafterStats.cp;
        this.availableCP = this.maxCP;
        this.state = 'NORMAL';
    }

    /**
     * Run the simulation.
     * @param {boolean} linear should everything be linear (aka no fail on actions, Initial preparations never procs)
     * @param maxTurns
     * @returns {ActionResult[]}
     */
    public run(linear = false, maxTurns = Infinity): SimulationResult {
        this.actions.forEach((action: CraftingAction, index: number) => {
            // If we're starting and the crafter is specialist
            if (index === 0 && this.crafterStats.specialist && this.crafterStats.level >= 70) {
                // Push stroke of genius buff
                this.buffs.push({
                    buff: Buff.STROKE_OF_GENIUS,
                    stacks: 0,
                    duration: Infinity,
                    appliedStep: -1
                });
                // Apply stroke of genius manually in the stats
                this.availableCP += 15;
                this.maxCP += 15;
            }
            // If we can use the action
            if (this.success === undefined && action.getBaseCPCost(this) <= this.availableCP && action.canBeUsed(this, linear)
                && this.steps.length < maxTurns) {
                this.runAction(action, linear);
            } else {
                // If we can't, add the step to the result but skip it.
                this.steps.push({
                    action: action,
                    success: null,
                    addedQuality: 0,
                    addedProgression: 0,
                    cpDifference: 0,
                    skipped: true,
                    solidityDifference: 0,
                    state: this.state,
                });
            }
            if (this.steps.length < maxTurns) {
                // Tick buffs after checking synth result, so if we reach 0 durability, synth fails.
                this.tickBuffs(linear);
            }
            // Tick state to change it for next turn if not in linear mode
            if (!linear) {
                this.tickState();
            }
        });
        // HQ percent to quality percent formulae: https://github.com/Ermad/ffxiv-craft-opt-web/blob/master/app/js/ffxivcraftmodel.js#L1455

        return {
            steps: this.steps,
            hqPercent: this.getHQPercent(),
            success: this.progression >= this.recipe.progress,
            simulation: this,
        };
    }

    /**
     * Runs an action, can be called from external class (Whistle for instance).
     * @param {CraftingAction} action
     * @param {boolean} linear
     */
    public runAction(action: CraftingAction, linear = false): void {
        // The roll for the current action's success rate, 0 if ideal mode, as 0 will even match a 1% chances.
        const probabilityRoll = linear ? 0 : Math.random() * 100;
        const qualityBefore = this.quality;<commit_msg>fix(simulator): fixed buffs duration not handled properly in snapshot mode<commit_after>import {Craft} from '../../../model/garland-tools/craft';
import {CraftingAction} from '../model/actions/crafting-action';
import {ActionResult} from '../model/action-result';
import {CrafterStats} from '../model/crafter-stats';
import {EffectiveBuff} from '../model/effective-buff';
import {Buff} from '../model/buff.enum';
import {SimulationResult} from './simulation-result';
import {SimulationReliabilityReport} from './simulation-reliability-report';

export class Simulation {

    public progression = 0;
    public quality = 0;
    public startingQuality = 0;
    // Durability of the craft
    public durability: number;

    public state: 'NORMAL' | 'EXCELLENT' | 'GOOD' | 'POOR';

    public availableCP: number;
    public maxCP: number;

    public buffs: EffectiveBuff[] = [];

    public success: boolean;

    public steps: ActionResult[] = [];

    constructor(public readonly recipe: Craft, public readonly actions: CraftingAction[], private _crafterStats: CrafterStats,
                hqIngredients: { id: number, amount: number }[] = []) {
        this.durability = recipe.durability;
        this.availableCP = this._crafterStats.cp;
        this.maxCP = this.availableCP;
        for (const ingredient of hqIngredients) {
            // Get the ingredient in the recipe
            const ingredientDetails = this.recipe.ingredients.find(i => i.id === ingredient.id);
            // Check that the ingredient in included in the recipe
            if (ingredientDetails !== undefined) {
                this.quality += ingredientDetails.quality * ingredient.amount;
            }
        }
        this.startingQuality = this.quality;
    }

    public getReliabilityReport(): SimulationReliabilityReport {
        const results: SimulationResult[] = [];
        // Let's run the simulation 500 times.
        for (let i = 0; i < 500; i++) {
            results.push(this.run(false));
            this.reset();
        }
        const successPercent = (results.filter(res => res.success).length / results.length) * 100;
        const hqPercent = results.reduce((p, c) => p + c.hqPercent, 0) / results.length;
        let hqMedian = 0;
        results.map(res => res.hqPercent).sort((a, b) => a - b);
        if (results.length % 2) {
            hqMedian = results[results.length / 2].hqPercent;
        } else {
            hqMedian = (results[Math.floor(results.length / 2)].hqPercent + results[Math.ceil(results.length / 2)].hqPercent) / 2;
        }
        return {
            rawData: results,
            successPercent: Math.round(successPercent),
            averageHQPercent: Math.round(hqPercent),
            medianHQPercent: hqMedian
        }
    }

    public getMinStats(): { control: number, craftsmanship: number, cp: number } {
        // Three loops, one per stat
        while (this.run(true).success) {
            this.crafterStats.craftsmanship--;
            this.reset();
        }
        while (this.run(true).success) {
            this.crafterStats._control--;
            this.reset();
        }
        while (this.run(true).success) {
            this.crafterStats.cp--;
            this.reset();
        }
        return {
            control: this.crafterStats.getControl(this),
            craftsmanship: this.crafterStats.craftsmanship,
            cp: this.crafterStats.cp
        };
    }

    public reset(): void {
        delete this.success;
        this.progression = 0;
        this.durability = this.recipe.durability;
        this.quality = this.startingQuality;
        this.buffs = [];
        this.steps = [];
        this.maxCP = this.crafterStats.cp;
        this.availableCP = this.maxCP;
        this.state = 'NORMAL';
    }

    /**
     * Run the simulation.
     * @param {boolean} linear should everything be linear (aka no fail on actions, Initial preparations never procs)
     * @param maxTurns
     * @returns {ActionResult[]}
     */
    public run(linear = false, maxTurns = Infinity): SimulationResult {
        this.actions.forEach((action: CraftingAction, index: number) => {
            // If we're starting and the crafter is specialist
            if (index === 0 && this.crafterStats.specialist && this.crafterStats.level >= 70) {
                // Push stroke of genius buff
                this.buffs.push({
                    buff: Buff.STROKE_OF_GENIUS,
                    stacks: 0,
                    duration: Infinity,
                    appliedStep: -1
                });
                // Apply stroke of genius manually in the stats
                this.availableCP += 15;
                this.maxCP += 15;
            }
            // If we can use the action
            if (this.success === undefined && action.getBaseCPCost(this) <= this.availableCP && action.canBeUsed(this, linear)
                && this.steps.length < maxTurns) {
                this.runAction(action, linear);
            } else {
                // If we can't, add the step to the result but skip it.
                this.steps.push({
                    action: action,
                    success: null,
                    addedQuality: 0,
                    addedProgression: 0,
                    cpDifference: 0,
                    skipped: true,
                    solidityDifference: 0,
                    state: this.state,
                });
            }
            if (this.steps.length <= maxTurns) {
                // Tick buffs after checking synth result, so if we reach 0 durability, synth fails.
                this.tickBuffs(linear);
            }
            // Tick state to change it for next turn if not in linear mode
            if (!linear) {
                this.tickState();
            }
        });
        // HQ percent to quality percent formulae: https://github.com/Ermad/ffxiv-craft-opt-web/blob/master/app/js/ffxivcraftmodel.js#L1455

        return {
            steps: this.steps,
            hqPercent: this.getHQPercent(),
            success: this.progression >= this.recipe.progress,
            simulation: this,
        };
    }

    /**
     * Runs an action, can be called from external class (Whistle for instance).
     * @param {CraftingAction} action
     * @param {boolean} linear
     */
    public runAction(action: CraftingAction, linear = false): void {
        // The roll for the current action's success rate, 0 if ideal mode, as 0 will even match a 1% chances.
        const probabilityRoll = linear ? 0 : Math.random() * 100;
        const qualityBefore = this.quality;
{'fraction_non_alphanumeric': 0.11193803159173754, 'fraction_numerical': 0.0069866342648845685, 'mean_word_length': 1.845505617977528, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 19, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4816534', 'n_tokens_mistral': 3765, 'n_tokens_neox': 3501, 'n_words': 942}