private void CreateFoodModel() { // avoid trivial constraints if (!FoodChoices.Any()) { return; } Model.AddConstraint(Expression.Sum(FoodChoices.Select(x => food[x])) <= 1, "use one food only"); foreach (var itm in FoodChoices) { var fd = itm.Food; var fv = food[itm]; // SIMPLIFICATION: no one cares about nq food. foreach (var prm in fd.Parameters) { var bp = prm.BaseParam; if (!RelevantStats.Contains(bp)) { continue; } var pvals = prm.Values.Where(v => v.Type == ParameterType.Hq).ToList(); // easy case, these just behave like normal gear // ASSUMPTION: each food provides either a fixed or relative buff for a given base param foreach (var pval in pvals.OfType <ParameterValueFixed>()) { FoodExprs.AddExprToDict(bp, pval.Amount * fv); } foreach (var pval in pvals.OfType <ParameterValueRelative>()) { // add relative modifier stat[bp] Model.AddConstraint(foodcap[itm, bp] <= pval.Amount * stat[bp], $"relative modifier for food {fd} in slot {bp}"); var limited = pval as ParameterValueRelativeLimited; if (limited != null) { Model.AddConstraint(foodcap[itm, bp] <= limited.Maximum * fv, $"cap for relative modifier for food {fd} in slot {bp}"); } FoodExprs.AddExprToDict(bp, foodcap[itm, bp]); } } } }
private void CreateGearModel() { foreach (var grp in GearChoices.GroupBy(g => g.EquipSlotCategory)) { // if gear is unique, equip it once only. grp.Where(e => e.IsUnique) .ForEach( e => Model.AddConstraint(Expression.Sum(grp.Key.PossibleSlots.Select(s => gear[s, e])) <= 1, $"ensure gear uniqueness for {e}")); // SIMPLIFICATION: we ignore blocked slots foreach (var s in grp.Key.PossibleSlots) { // choose at most one gear per slot Model.AddConstraint(Expression.Sum(grp.Select(e => gear[s, e])) <= 1, $"choose at most one item for slot {s}"); foreach (var e in grp) { var gv = gear[s, e]; // ASSUMPTION: all gear choices have fixed parameters e.AllParameters.Where(p => RelevantStats.Contains(p.BaseParam)) .ForEach( p => AddExprToDict(StatExprs, p.BaseParam, p.Values.Sum(v => ((ParameterValueFixed)v).Amount) * gv)); // ASSUMPTION: all meldable items have at least one materia slot // ASSUMPTION: customisable relics are unmeldable if (e.FreeMateriaSlots > 0) { CreateMateriaModel(s, e); } else if (RelicCaps != null && RelicCaps.ContainsKey(e)) { CreateRelicModel(s, e); } } } } }
/// <summary> /// Creates a new BiS solver model. /// </summary> /// <param name="solverConfig">Solver configuration</param> /// <param name="job">Job to solve for</param> /// <param name="gearChoices">Gear items to choose from. Keep this small.</param> /// <param name="foodChoices">List of food choices. This can be fairly large, it doesn't add much complexity.</param> /// <param name="materiaChoices"> /// Dictionary of materia choices; set value to true if the materia is allowed for advanced /// melding. The materia with the highest eligible stat value is chosen. (Note that Tier is 0-indexed) /// </param> //TODO: this is getting out of hand. need config object asap. //TODO: make model parts pluggable if possible public BisModel(SolverConfig solverConfig, ClassJob job, IEnumerable <Equipment> gearChoices, IEnumerable <FoodItem> foodChoices, IDictionary <MateriaItem, bool> materiaChoices) { Model = new Model(); SolverConfig = solverConfig; Job = job; JobConfig = SolverConfig.JobConfigs[Job]; GearChoices = gearChoices.ToList(); FoodChoices = foodChoices.ToList(); // collect stats we care about RelevantStats = JobConfig.Weights.Keys.Union(JobConfig.StatRequirements.Keys).ToList(); // we don't care about materia which affect unneeded stats MateriaChoices = materiaChoices.Where(m => RelevantStats.Contains(m.Key.BaseParam)) .ToDictionary(k => k.Key, k => k.Value); var allEquipSlots = GearChoices.SelectMany(g => g.EquipSlotCategory.PossibleSlots).ToList(); gear = new VariableCollection <EquipSlot, Equipment>(Model, allEquipSlots, GearChoices, type: VariableType.Binary, debugNameGenerator: (s, e) => new StringBuilder().AppendFormat("{0}_{1}", s, e), lowerBoundGenerator: (s, e) => CheckRequired(e) ? 1 : 0); food = new VariableCollection <FoodItem>(Model, FoodChoices, type: VariableType.Binary, debugNameGenerator: e => new StringBuilder().AppendFormat("{0}", e.Item), lowerBoundGenerator: i => CheckRequired(i.Item) ? 1 : 0); foodcap = new VariableCollection <FoodItem, BaseParam>(Model, FoodChoices, RelevantStats, type: VariableType.Integer, debugNameGenerator: (i, bp) => new StringBuilder().AppendFormat("{0}_{1}_cap", i.Item, bp), lowerBoundGenerator: (x, b) => 0); materia = new VariableCollection <EquipSlot, Equipment, MateriaItem>(Model, allEquipSlots, GearChoices, MateriaChoices.Keys, type: VariableType.Integer, debugNameGenerator: (s, e, m) => new StringBuilder().AppendFormat("{2}_{0}_{1}", s, e, m), lowerBoundGenerator: (s, e, bp) => 0, upperBoundGenerator: (s, e, b) => e.TotalMateriaSlots()); cap = new VariableCollection <EquipSlot, Equipment, BaseParam>(Model, allEquipSlots, GearChoices, RelevantStats, type: VariableType.Integer, debugNameGenerator: (s, e, bp) => new StringBuilder().AppendFormat("{2}_cap_{0}_{1}", s, e, bp), lowerBoundGenerator: (s, e, b) => 0); relicBase = new VariableCollection <EquipSlot, Equipment, BaseParam>(Model, allEquipSlots, GearChoices, RelevantStats, type: VariableType.Integer, debugNameGenerator: (s, e, bp) => new StringBuilder().AppendFormat("{2}_relic_base_{0}_{1}", s, e, bp), lowerBoundGenerator: (s, e, b) => 0); stat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, debugNameGenerator: bp => new StringBuilder().AppendFormat("gear_materia__{0}", bp), lowerBoundGenerator: x => 0); modstat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, debugNameGenerator: bp => new StringBuilder().AppendFormat("added_food_{0}", bp), lowerBoundGenerator: x => 0); allocstat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, debugNameGenerator: bp => new StringBuilder().AppendFormat("allocated_{0}", bp), lowerBoundGenerator: x => 0, upperBoundGenerator: x => SolverConfig.AllocatedStatsCap); tieredstat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, debugNameGenerator: bp => new StringBuilder().AppendFormat("tiered_{0}", bp), lowerBoundGenerator: x => 0); Model.AddConstraint( Expression.Sum(RelevantStats.Where(bp => MainStats.Contains(bp.Name)).Select(bp => allocstat[bp])) <= SolverConfig.AllocatedStatsCap, "cap allocatable stats"); StatExprs = RelevantStats.ToDictionary(bp => bp, bp => Expression.EmptyExpression); FoodExprs = RelevantStats.ToDictionary(bp => bp, bp => (Expression)stat[bp]); SolverConfig.BaseStats.ForEach(kv => StatExprs[kv.Key] = kv.Value + Expression.EmptyExpression); CreateGearModel(); CreateMateriaOrdering(); CreateFoodModel(); CreateTiers(); CreateObjective(); }
private void CreateGearModel() { foreach (var grp in GearChoices.GroupBy(g => g.EquipSlotCategory)) { // if gear is unique, equip it once only. if (grp.Key.PossibleSlots.Count() > 1) { grp.Where(e => e.IsUnique) .ForEach( e => Model.AddConstraint(Expression.Sum(grp.Key.PossibleSlots.Select(s => gear[s, e])) <= 1, $"ensure gear uniqueness for {e}")); } // SIMPLIFICATION: we ignore blocked slots foreach (var s in grp.Key.PossibleSlots) { // choose at most one gear per slot Model.AddConstraint(Expression.Sum(grp.Select(e => gear[s, e])) <= 1, $"choose at most one item for slot {s}"); foreach (var e in grp) { var gv = gear[s, e]; // ASSUMPTION: all gear choices have fixed parameters var stats = e.AllParameters.Where(p => RelevantStats.Contains(p.BaseParam)) .ToDictionary(p => p.BaseParam, p => p.Values.OfType <ParameterValueFixed>().Select(val => val.Amount).Sum()); if (SolverConfig.EquipmentOverrides?.ContainsKey(e) ?? false) { var statsOverride = SolverConfig.EquipmentOverrides[e]; if (statsOverride == null) { continue; } foreach (var kv in statsOverride) { stats[kv.Key] = kv.Value; } } // sanity check stats foreach (var kv in stats) { if (e.GetMaximumParamValue(kv.Key) < kv.Value) { Console.Error.WriteLine($"{kv.Key} => {kv.Value} for {e} is out of range"); } } stats.ForEach(p => StatExprs.AddExprToDict(p.Key, p.Value * gv)); // ASSUMPTION: all meldable items have at least one materia slot // ASSUMPTION: customisable relics are unmeldable if (e.FreeMateriaSlots > 0) { CreateMateriaModel(s, e); } else { CreateRelicModel(s, e); } } } } }
/// <summary> /// Creates a new BiS solver model. /// </summary> /// <param name="weights">Stat weights. The higher the weight, the more a stat is desirable</param> /// <param name="statReqs">Minimum amount of stats that must be present in a solution</param> /// <param name="baseStats">Stats of a character without any gear</param> /// <param name="gearChoices">Gear items to choose from. Keep this small.</param> /// <param name="foodChoices">List of food choices. This can be fairly large, it doesn't add much complexity.</param> /// <param name="materiaChoices"> /// Dictionary of materia choices; set value to true if the materia is allowed for advanced /// melding. The materia with the highest eligible stat value is chosen. (Note that Tier is 0-indexed) /// </param> /// <param name="relicCaps">Designates customizable relics. Value of an entry determines the total stat cap.</param> /// <param name="overmeldThreshold"> /// Extend the overmelding threshold --- i.e. if you set overmeldThreshold to n, materia /// from materiaChoices that isn't normally allowed in advanced melds can be used up to n times in advanced meld /// </param> /// <param name="allocStatCap">Cap for allocatable stats. Default is 35</param> /// <param name="maximizeUnweightedValues">Maximize unweighted values with a small weight (1e-5)</param> //TODO: this is getting out of hand. need config object asap. //TODO: make model parts pluggable if possible public BisModel(IDictionary <BaseParam, double> weights, IDictionary <BaseParam, int> statReqs, IDictionary <BaseParam, int> baseStats, IEnumerable <Equipment> gearChoices, IEnumerable <FoodItem> foodChoices, IDictionary <MateriaItem, bool> materiaChoices, IDictionary <Equipment, int> relicCaps = null, int overmeldThreshold = 0, int allocStatCap = 35, bool maximizeUnweightedValues = true) { Model = new Model(); StatRequirements = statReqs; Weights = weights; GearChoices = gearChoices.ToList(); FoodChoices = foodChoices.ToList(); RelicCaps = relicCaps; OvermeldThreshold = overmeldThreshold; MaximizeUnweightedValues = maximizeUnweightedValues; // collect stats we care about RelevantStats = Weights.Keys.Union(StatRequirements.Keys).ToList(); // we don't care about materia which affect unneeded stats MateriaChoices = materiaChoices.Where(m => RelevantStats.Contains(m.Key.BaseParam)) .ToDictionary(k => k.Key, k => k.Value); var allEquipSlots = GearChoices.SelectMany(g => g.EquipSlotCategory.PossibleSlots).ToList(); gear = new VariableCollection <EquipSlot, Equipment>(Model, allEquipSlots, GearChoices, type: VariableType.Binary); food = new VariableCollection <FoodItem>(Model, FoodChoices, type: VariableType.Binary); foodcap = new VariableCollection <FoodItem, BaseParam>(Model, FoodChoices, RelevantStats, type: VariableType.Integer, lowerBoundGenerator: (x, b) => 0); materia = new VariableCollection <EquipSlot, Equipment, MateriaItem>(Model, allEquipSlots, GearChoices, MateriaChoices.Keys, type: VariableType.Integer, lowerBoundGenerator: (s, e, bp) => 0); cap = new VariableCollection <EquipSlot, Equipment, BaseParam>(Model, allEquipSlots, GearChoices, RelevantStats, type: VariableType.Integer, lowerBoundGenerator: (s, e, b) => 0); stat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, lowerBoundGenerator: x => 0); modstat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, lowerBoundGenerator: x => 0); allocstat = new VariableCollection <BaseParam>(Model, RelevantStats, type: VariableType.Integer, lowerBoundGenerator: x => 0); Model.AddConstraint( Expression.Sum(RelevantStats.Where(bp => MainStats.Contains(bp.Name)).Select(bp => allocstat[bp])) <= allocStatCap, "cap allocatable stats"); StatExprs = RelevantStats.ToDictionary(bp => bp, bp => Expression.EmptyExpression); FoodExprs = RelevantStats.ToDictionary(bp => bp, bp => (Expression)stat[bp]); baseStats.ForEach(kv => StatExprs[kv.Key] = kv.Value + Expression.EmptyExpression); var bigM = 50 * GearChoices.Select( g => g.AllParameters.Select( p => p.Values.OfType <ParameterValueFixed>().Select(v => v.Amount).Max()) .Max()).Max(); CreateGearModel(); CreateFoodModel(); CreateObjective(); }