Changeset - d6859ea7177f
[Not reviewed]
default
0 1 0
Jason Maltzen - 3 years ago 2021-09-10 00:02:16
jason@hiddenachievement.com
Add some sanity checks on min/max concentration and min/max reagents in recipe generation. Also, clear out the search queue when generation has finished so the queue isn't loaded next time. This fixes the start/resume button state when entering the recipe generator after a prior run had finished.
1 file changed with 12 insertions and 0 deletions:
0 comments (0 inline, 0 general)
Services/RecipeGenerator.cs
Show inline comments
...
 
@@ -93,192 +93,198 @@ namespace DesertPaintCodex.Services
 
            set => _log = value != null ? new StreamWriter(value) : null;
 
        }
 

	
 
        public void FlushLog()
 
        {
 
            _log?.Flush();
 
        }
 

	
 
        public void CloseLog()
 
        {
 
            _log?.Close();
 
            _log = null;
 
        }
 

	
 
        private void WriteLog(string msg)
 
        {
 
            if (_log == null) return;
 
            lock (_workerLock)
 
            {
 
                _log.WriteLine(msg);
 
            }
 
        }
 

	
 
        private void WriteLog(string msg, params object[] args)
 
        {
 
            if (_log == null) return;
 
            lock(_workerLock)
 
            {
 
                _log.WriteLine(msg, args);
 
            }
 
        }
 

	
 
        private class ReagentCostSort : IComparer<Reagent>
 
        {
 
            public int Compare(Reagent? reagent1, Reagent? reagent2)
 
            {
 
                if (reagent1 == null)
 
                {
 
                    if (reagent2 == null) return 0;
 
                    return -1;
 
                }
 
                if (reagent2 == null) return 1;
 
                
 
                return (int)reagent1.Cost - (int)reagent2.Cost;
 
            }
 
        }
 

	
 
        public void InitRecipes(Dictionary<string, PaintRecipe> initialRecipes)
 
        {
 
            if (_running)
 
            {
 
                return;
 
            }
 
            _recipeCosts.Clear();
 
            _recipes.Clear();
 
            foreach (PaintRecipe recipe in initialRecipes.Values)
 
            {
 
                //PaintRecipe recipeCopy = new PaintRecipe(recipe);
 
                AddCheapestRecipe(recipe);
 
            }
 
        }
 

	
 
        private void InitSortedReagents()
 
        {
 
            _costSortedReagents.Clear();
 
            foreach (string name in ReagentService.Names)
 
            {
 
                Reagent reagent = ReagentService.GetReagent(name);
 
                _costSortedReagents.Add(reagent);
 
            }
 
            _costSortedReagents.Sort(new ReagentCostSort());
 
        }
 

	
 
        // Generate paint recipes.
 
        // minConcentration - the minimum permitted concentration in a recipe (10 for paint, 50 for ribbons)
 
        // maxConcentration - the maximum concentration for a recipe
 
        // minReagents - the minimum number of ingredients in a recipe
 
        // maxReagents - the maximum number of ingredients allowed in a recipe
 
        // fullQuantityDepth - at this depth of ingredient or below, allow up to fullQuantity of any reagent
 
        // fullQuantity - the maximum amount of any reagent to permit up to the full quantity depth. After that, reagents are limited by the
 
        //                per-reagent value
 
        public void BeginRecipeGeneration(uint minConcentration, uint maxConcentration, uint minReagents, uint maxReagents, uint fullQuantityDepth, uint fullQuantity)
 
        {
 
            if (_running)
 
            {
 
                // Already running - don't start again
 
                return;
 
            }
 

	
 
            MinConcentration = minConcentration;
 
            MaxConcentration = maxConcentration;
 
            MinReagents = minReagents;
 
            MaxReagents = maxReagents;
 
            FullQuantity = fullQuantity;
 
            FullQuantityDepth = fullQuantityDepth;
 

	
 
            // Sanity check
 
            if (MinConcentration < 10) MinConcentration = 10;
 
            if (MaxConcentration < MinConcentration) MaxConcentration = MinConcentration;
 
            if (MinReagents < 1) MinReagents = 1;
 
            if (MaxReagents < MinReagents) MaxReagents = MinReagents;
 

	
 
            // first, sort reagents by cost.
 
            InitSortedReagents();
 

	
 
            _totalReagents = (uint)_costSortedReagents.Count;
 

	
 
            // Pre-populate recipes list with:
 
            // 1) 1-ingredient recipes @ min concentration for all enabled ingredients with a count >= min concentration
 
            // 2) any previously-generated recipes
 
            WriteLog($"===================== BEGIN RECIPE GENERATION ========================== {DateTime.Now}");
 
            WriteLog("Pre-populating basic recipes.");
 
            int enabledReagentCount = 0;
 
            PaintRecipe recipe = new();
 
            foreach (var reagent in _costSortedReagents.Where(reagent => reagent.Enabled))
 
            {
 
                if (!reagent.IsCatalyst && ((reagent.RecipeMax >= minConcentration) || ((FullQuantityDepth > 0) && (FullQuantity >= minConcentration))))
 
                {
 
                    recipe.Clear();
 
                    recipe.AddReagent(reagent.Name, minConcentration);
 
                    AddCheapestRecipe(recipe);
 
                }
 
                ++enabledReagentCount;
 
            }
 
            MaxReagents = (uint)Math.Min(enabledReagentCount, MaxReagents);
 
            MinReagents = Math.Min(MinReagents, MaxReagents);
 
            
 
            _searchQueue.Clear();
 

	
 
            for (uint reagentIdx = 0; reagentIdx < _costSortedReagents.Count; ++reagentIdx)
 
            {
 
                if (!_costSortedReagents[(int) reagentIdx].Enabled) continue;
 
                
 
                // queue up all combinations of MinReagents
 
                RecipeSearchNode initialNode = new(_costSortedReagents, reagentIdx)
 
                {
 
                    FullQuantity      = FullQuantity,
 
                    FullQuantityDepth = FullQuantityDepth,
 
                    MinConcentration  = minConcentration,
 
                    MaxConcentration  = maxConcentration,
 
                    MinReagents       = minReagents,
 
                    MaxReagents       = maxReagents
 
                };
 
                initialNode.WriteLog = WriteLog;
 

	
 
                if (MinReagents > 1)
 
                {
 
                    while (NextReagentSetBreadthFirst(initialNode, 1, minReagents))
 
                    {
 
                        if (initialNode.ReagentCount != minReagents) continue;
 
                        
 
                        //Console.WriteLine("Initial node at size {0}/{1} with recipe: {2}", initialNode.ReagentCount, minReagents, initialNode.TestRecipe.ToString());
 
                        RecipeSearchNode searchNode = new RecipeSearchNode(initialNode);
 
                        searchNode.WriteLog = WriteLog;
 
                        _searchQueue.Enqueue(searchNode);
 
                    }
 
                }
 
                else
 
                {
 
                    _searchQueue.Enqueue(initialNode);
 
                }
 
            }
 

	
 
            _recipeCount = 0;
 

	
 
            WriteLog("Begin recipe generation: MaxConcentration={0} MinReagents={1} MaxReagents={2} FullQuantity={3} FullQuantityDepth={4}", MaxConcentration, MinReagents, MaxReagents, FullQuantity, FullQuantityDepth);
 

	
 
            // start worker threads to do the actual work
 
            ResumeRecipeGeneration();
 
        }
 

	
 
        public void ResumeRecipeGeneration()
 
        {
 
            if (_running)
 
            {
 
                // Already running - don't start again
 
                return;
 
            }
 
            _running = true;
 
            _requestCancel = false;
 

	
 
            WriteLog("Resuming recipe generation: pre-threads={0} reagent count={1} search queue={2}", _runningThreads, _costSortedReagents.Count, _searchQueue.Count);
 
            _runningThreads = 0; // presumably!
 

	
 
            int threadCount = Math.Min(Math.Min(_costSortedReagents.Count, _searchQueue.Count), (int)MaxThreads);
 
            if (threadCount == 0)
 
            {
 
                Finished?.Invoke(this, EventArgs.Empty);
 
            }
 
            _generatorThreads.Clear();
 
            Console.WriteLine("Starting {0} generator threads.", threadCount);
 
            WriteLog($"===== Starting {threadCount} threads.");
 
            for (int i = 0; i < threadCount; ++i)
 
            {
 
                Thread thr = new(Generate) {Priority = ThreadPriority.BelowNormal};
 
                _generatorThreads.Add(thr);
 
            }
 
            foreach (Thread thr in _generatorThreads)
...
 
@@ -428,192 +434,198 @@ namespace DesertPaintCodex.Services
 
                            break;
 
                        default:
 
                            success = false;
 
                            break;
 
                    }
 
                }
 
                else
 
                {
 
                    success = false;
 
                    break;
 
                }
 
            }
 
            return success;
 
        }
 

	
 
        private void Generate()
 
        {
 
            lock(_workerLock)
 
            {
 
                ++_runningThreads;
 
            }
 

	
 
            bool ok;
 
            do
 
            {
 
                RecipeSearchNode? node;
 
                lock (_workerLock)
 
                {
 
                    ok = _searchQueue.TryDequeue(out node);
 
                }
 

	
 
                if (!ok) break;
 
                
 
                Debug.Assert(node != null);
 

	
 
                node.WriteLog = WriteLog;
 
                
 
                if (Mode == SearchType.DepthFirst)
 
                {
 
                    uint targetQuantity = (node.ReagentCount <= FullQuantityDepth)
 
                        ? ((uint)node.ReagentCount * FullQuantity) 
 
                        : node.MaxConcentration + 1;
 
                    do {
 
                        --targetQuantity;
 
                        if (node.ShouldLog)
 
                        {
 
                            WriteLog($" == Initializing {node} for quantity {targetQuantity}");
 
                        }
 
                        node.InitForQuantity(targetQuantity);
 
                    } while (targetQuantity > MinConcentration && (node.CurrentTargetQuantity != node.UsedQuantity));
 
    
 
                    while ((ok = IterateDepthFirst(node)) && !_requestCancel)
 
                    {
 
                        Progress?.Invoke(this, EventArgs.Empty);
 
                    }
 
                }
 
                else
 
                {
 
                    // breadth-first search
 
                    uint targetQuantity = MinConcentration - 1;
 
                    uint quantityLimit = (node.ReagentCount <= FullQuantityDepth) 
 
                        ? (FullQuantity * (uint)node.ReagentCount) 
 
                        : node.MaxConcentration;
 
                    do {
 
                        ++targetQuantity;
 
                        if (node.ShouldLog)
 
                        {
 
                            WriteLog($" == Initializing {node} for quantity {targetQuantity}");
 
                        }
 
                        node.InitForQuantity(targetQuantity);
 
                    } while ((targetQuantity < quantityLimit) && (node.CurrentTargetQuantity != node.UsedQuantity));
 
    
 
                    while ((ok = IterateBreadthFirst(node)) && !_requestCancel)
 
                    {
 
                        Progress?.Invoke(this, EventArgs.Empty);
 
                    }
 
                }
 
                if (ok)
 
                {
 
                    // stopped because cancel was requested - requeue the node in its current state for resume
 
                    _searchQueue.Enqueue(node);
 
                }
 
            } while (!_requestCancel);
 

	
 
            bool done;
 
            lock(_workerLock)
 
            {
 
                --_runningThreads;
 
                //generatorThreads.Remove(Thread.CurrentThread);
 

	
 
                done = (_runningThreads == 0);
 
            }
 

	
 
            if (!done) return;
 
            
 
            _running       = false;
 
            if (!_requestCancel && !_searchQueue.IsEmpty)
 
            {
 
                Debug.WriteLine($"Recipe generation complete, but search queue isn't empty {_searchQueue.Count}.");
 
                WriteLog($"Recipe generation complete, but search queue isn't empty {_searchQueue.Count}.");
 
                _searchQueue.Clear();
 
            }
 
            _requestCancel = false;
 
            Finished?.Invoke(this, EventArgs.Empty);
 
        }
 

	
 
        // Add the cheapest recipe to the recipe list
 
        // returns the discarded recipe from the pair (or null if no original recipe to replace)
 
        private void AddCheapestRecipe(PaintRecipe recipe)
 
        {
 
            if (!recipe.IsValidForConcentration(MinConcentration)) return;
 
            
 
            string colorName = PaletteService.FindNearest(recipe.ReactedColor);
 
            lock (_workerLock)
 
            {
 
                uint newCost = recipe.Cost;
 
                if (_recipeCosts.TryGetValue(colorName, out var cost))
 
                {
 
                    if (cost < newCost)
 
                    {
 
                        // WriteLog($"Skipping recipe (cost {newCost} > {cost}): {recipe}");
 
                        return;
 
                    }
 
                    PaintRecipe origRecipe = _recipes[colorName];
 
                    if (cost == newCost && recipe.Concentration >= origRecipe.Concentration)
 
                    {
 
                        // Same cost, greater concentration - use the lower concentration recipe
 
                        WriteLog($"Skipping recipe (cost {newCost}, {recipe.Concentration} >= {origRecipe.Concentration}): {recipe}");
 
                        return;
 
                    }
 
                        
 
                    _recipes[colorName].CopyFrom(recipe); // Copy recipe because the one passed in should be const and will get overwritten
 
                    _recipeCosts[colorName] = newCost;
 
                    WriteLog($"Replacing recipe (cost {newCost} < {cost}): {recipe}");
 
                        
 
                    if (NewRecipe == null) return;
 
                        
 
                    NewRecipeEventArgs args = new(colorName, _recipes[colorName]);
 
                    NewRecipe(this, args);
 
                }
 
                else
 
                {
 
                    // This would be an error!
 
                    _recipeCosts.Add(colorName, newCost);
 
                    PaintRecipe newRecipe = new PaintRecipe(recipe); // Copy recipe because the one passed in should be const and will get overwritten
 
                    _recipes.Add(colorName, newRecipe);
 

	
 
                    WriteLog($"New recipe (cost {newCost}): {recipe}");
 

	
 
                    if (NewRecipe == null) return;
 
                        
 
                    NewRecipeEventArgs args = new(colorName, newRecipe);
 
                    NewRecipe(this, args);
 
                }
 
            }
 
        }
 

	
 
        private bool IterateDepthFirst(RecipeSearchNode node)
 
        {
 
            TestCurrentRecipe(node);
 

	
 
            // pick recipe quantities at current recipe ingredients/size
 
            if (NextRecipe(node))
 
            {
 
                lock(_workerLock)
 
                {
 
                    ++_recipeCount;
 
                }
 
                //System.Console.WriteLine("Found next recipe at size {0} qty {1}", node.Reagents.Count, node.CurrentTargetQuantity);
 
                return true;
 
            }
 

	
 
            if (NextRecipeSize(node))
 
            {
 
                //System.Console.WriteLine("Found next recipe size {0}", node.CurrentTargetQuantity);
 
                return true;
 
            }
 

	
 
            // Search for next ingredient combo - all quantity combos for previous were searched
 
            //System.Console.WriteLine("Finding next ingredient combo");
 
            do
 
            {
 
                if (node.AddNextReagent()) continue;
 
                
 
                while ((node.ReagentCount > node.MinReagents) && (node.LastReagent == (_totalReagents-1)))
 
                {
 
                    node.RemoveLastReagent();
 
                }
 
                if (node.ReagentCount == node.MinReagents)
 
                {
 
                    // done
 
                    return false;
 
                }
 
                uint nextReagent = node.NextFreeReagent(node.LastReagent);
 
                while ((node.ReagentCount > node.MinReagents) && (nextReagent >= _totalReagents))
 
                {
 
                    // No more reagents to try at this level
 
                    node.RemoveLastReagent();
0 comments (0 inline, 0 general)