// Helper, returns the number of words that do not intersect with any other word public int WordsNotConnectedCount() { List <WordPosition> tempList = new List <WordPosition>(m_WordPositionList); int blocksCount = 0; for (; ;) { if (tempList.Count == 0) { break; } // Start a new block of connected WordPosition blocksCount++; WordPosition wordPosition = tempList[0]; tempList.RemoveAt(0); foreach (WordPosition w in GetConnectedWordPositions(wordPosition, tempList)) { if (tempList.Contains(w)) { tempList.Remove(w); } } } return(blocksCount); }
private void RemoveSquares(WordPosition wordPosition) { Debug.Assert(wordPosition != null); int row = wordPosition.StartRow; int column = wordPosition.StartColumn; for (int i = 0; i < wordPosition.Word.Length; i++) { Square sq = GetSquare(row, column); Debug.Assert(sq != null); if (sq.ShareCount == 1) { m_Squares.Remove(Index(row, column)); } else { sq.ShareCount--; } if (wordPosition.IsVertical) { row++; } else { column++; } } }
// Private helper private void AddSquares(WordPosition wordPosition) { Debug.Assert(wordPosition != null); int row = wordPosition.StartRow; int column = wordPosition.StartColumn; foreach (char c in wordPosition.Word) { if (GetSquare(row, column) == null) { Square sq = new Square(row, column, c, false, 1); m_Squares.Add(Index(row, column), sq); } else { Square sq = GetSquare(row, column); Debug.Assert(sq.Letter == c); sq.ShareCount++; } if (wordPosition.IsVertical) { row++; } else { column++; } } }
/// <summary>Core placement function, adds a list of words to current layout</summary> /// <param name="wordsToAddList">List of words to place</param> /// <param name="withLayoutBackup">If true, current layout is backed up, and restored if placement of all words failed</param> /// <returns>Returns a list of WordPosition for placed words in case of success, or false if placement failed, current layout is preserved in this case</returns> public IEnumerable <WordPosition> PlaceWordsList(IEnumerable <string> wordsToAddList, bool withLayoutBackup) { if (wordsToAddList == null) { throw new ArgumentNullException(nameof(wordsToAddList)); } // Keep a copy of current layout to restore if placement fails at some point WordPositionLayout backupLayout = null; if (withLayoutBackup) { backupLayout = new WordPositionLayout(Layout); } string checkMessage = CheckWordsList(wordsToAddList); if (!string.IsNullOrEmpty(checkMessage)) { throw new BonzaException(checkMessage); } List <string> shuffledList = new List <string>(wordsToAddList).Shuffle(); List <WordPosition> placedWordPositionList = new List <WordPosition>(); while (shuffledList.Count > 0) { List <string> placedWords = new List <string>(); foreach (string word in shuffledList) { WordPosition wordPosition = PlaceWord(word); if (wordPosition != null) { placedWords.Add(word); placedWordPositionList.Add(wordPosition); //Debug.WriteLine($"Placed {placedWordPositionList.Count}/{wordsToAddList.Count}: {wordPosition.Word}"); } } // If at the end of this loop no canonizedWord has been placed, we have a problem... if (placedWords.Count == 0) { if (withLayoutBackup) { Layout = backupLayout; // Restore initial layout } return(null); } // On the other hand, if pass was successful, remove all placed words and go for a new pass foreach (string placedWord in placedWords) { shuffledList.Remove(placedWord); } } return(placedWordPositionList); }
// Try to place a word in current layout, following rules of puzzle layout // If withTooClose if false, a too close condition returns Invalid // Part of public interface public PlaceWordStatus CanPlaceWord(WordPosition wordPosition, bool withTooClose) { if (wordPosition == null) { throw new ArgumentNullException(nameof(wordPosition)); } return(wordPosition.IsVertical ? CanPlaceVerticalWord(wordPosition, withTooClose) : CanPlaceHorizontalWord(wordPosition, withTooClose)); }
// Low-level removal function, public public void RemoveWordPosition(WordPosition wordPosition) { if (wordPosition == null) { throw new ArgumentNullException(nameof(wordPosition)); } if (!m_WordPositionList.Contains(wordPosition)) { throw new ArgumentException("WordPosition not in the layout"); } RemoveSquares(wordPosition); m_WordPositionList.Remove(wordPosition); }
// Core function, adds a canonizedWord to current layout and place it // Returns WordPosition of placed word, or null if the canonizedWord couldn't be placed private WordPosition PlaceWord(string originalWord) { var l = FindWordPossiblePlacements(originalWord, PlaceWordOptimization.High); if (l == null || l.Count == 0) { return(null); } WordPosition wordPosition = l.TakeRandom(); Layout.AddWordPositionNoCheck(wordPosition); return(wordPosition); }
// Safe version, public public PlaceWordStatus AddWordPosition(WordPosition wordPosition) { if (wordPosition == null) { throw new ArgumentNullException(nameof(wordPosition)); } if (m_WordPositionList.Contains(wordPosition)) { throw new ArgumentException("WordPosition already in the layout"); } var res = CanPlaceWord(wordPosition, true); if (res != PlaceWordStatus.Invalid) { AddWordPositionNoCheck(wordPosition); } return(res); }
// Return layout bounds extended with a WordPosition added // Don't use Position version of BoundingRectangle constructor, too slow internal static BoundingRectangle ExtendBounds(BoundingRectangle r, WordPosition wordPosition) { if (wordPosition.IsVertical) { return(new BoundingRectangle( Math.Min(r.Min.Row, wordPosition.StartRow), Math.Max(r.Max.Row, wordPosition.StartRow + wordPosition.Word.Length - 1), Math.Min(r.Min.Column, wordPosition.StartColumn), Math.Max(r.Max.Column, wordPosition.StartColumn))); } else { return(new BoundingRectangle( Math.Min(r.Min.Row, wordPosition.StartRow), Math.Max(r.Max.Row, wordPosition.StartRow), Math.Min(r.Min.Column, wordPosition.StartColumn), Math.Max(r.Max.Column, wordPosition.StartColumn + wordPosition.Word.Length - 1))); } }
// Private version, returns a list of WordPosition that wordPosition is connected to from wordPositionList private static List <WordPosition> GetConnectedWordPositions(WordPosition wordPosition, List <WordPosition> wordPositionList) { List <WordPosition> tempList = new List <WordPosition>(wordPositionList); Stack <WordPosition> toExamine = new Stack <WordPosition>(); List <WordPosition> connected = new List <WordPosition>(); toExamine.Push(wordPosition); if (tempList.Contains(wordPosition)) { tempList.Remove(wordPosition); } while (toExamine.Count > 0) { WordPosition w1 = toExamine.Pop(); if (!connected.Contains(w1)) { if (wordPosition != w1) { connected.Add(w1); } if (tempList.Contains(w1)) { tempList.Remove(w1); } foreach (var w2 in tempList) { if (DoWordsIntersect(w1, w2) && !connected.Contains(w2)) { toExamine.Push(w2); } } } } return(connected); }
// Low-level function to add a WordPosition to Layout, do not check that placement is correct public void AddWordPositionNoCheck(WordPosition wordPosition) { m_WordPositionList.Add(wordPosition); AddSquares(wordPosition); }
// Returns true if the two WordPosition intersect private static bool DoWordsIntersect(WordPosition word1, WordPosition word2) { if (word1.IsVertical && word2.IsVertical) { // Both vertical, different column: no problem if (word1.StartColumn != word2.StartColumn) { return(false); } // On the same column, check that one row ends before the other starts // ReSharper disable once ConvertIfStatementToReturnStatement if (word1.StartRow + word1.Word.Length - 1 < word2.StartRow || word2.StartRow + word2.Word.Length - 1 < word1.StartRow) { return(false); } // They overlap, it's not really an intersection but still count as one return(true); } if (!word1.IsVertical && !word2.IsVertical) { // Both horizontal, different row, no problem if (word1.StartRow != word2.StartRow) { return(false); } // On the same row, check that one column ends before the other starts // ReSharper disable once ConvertIfStatementToReturnStatement if (word1.StartColumn + word1.Word.Length - 1 < word2.StartColumn || word2.StartColumn + word2.Word.Length - 1 < word1.StartColumn) { return(false); } // Overlap of two horizontal words return(true); } if (!word1.IsVertical && word2.IsVertical) { // word1 horizontal, word2 vertical // if word2 column does not overlap with word1 columns, no problem if (word2.StartColumn < word1.StartColumn || word2.StartColumn > word1.StartColumn + word1.Word.Length - 1) { return(false); } // If word2 rows do now overlap with word1 row, no problem // ReSharper disable once ConvertIfStatementToReturnStatement if (word1.StartRow < word2.StartRow || word1.StartRow > word2.StartRow + word2.Word.Length - 1) { return(false); } // Otherwise we have an intersection return(true); } { // word1 vertical, word2 horizontal // if word1 column does not overlap with word2 columns, no problem if (word1.StartColumn < word2.StartColumn || word1.StartColumn > word2.StartColumn + word2.Word.Length - 1) { return(false); } // If word1 rows do now overlap with word2 row, no problem // ReSharper disable once ConvertIfStatementToReturnStatement if (word2.StartRow < word1.StartRow || word2.StartRow > word1.StartRow + word1.Word.Length - 1) { return(false); } // Otherwise we have an intersection return(true); } }
// Public version, returns words connected to wordPosition (not including wordPosition) from current layout public IEnumerable <WordPosition> GetConnectedWordPositions(WordPosition wordPosition) { return(GetConnectedWordPositions(wordPosition, m_WordPositionList)); }
// Helper to reduce cyclomatic complexity private PlaceWordStatus CanPlaceHorizontalWord(WordPosition wordPosition, bool withTooClose) { PlaceWordStatus result = PlaceWordStatus.Valid; int row = wordPosition.StartRow; int column = wordPosition.StartColumn; // Free cell left if (IsOccupiedSquare(row, column - 1)) { if (withTooClose) { result = PlaceWordStatus.TooClose; } else { return(PlaceWordStatus.Invalid); } } // Free cell right if (IsOccupiedSquare(row, column + wordPosition.Word.Length)) { if (withTooClose) { result = PlaceWordStatus.TooClose; } else { return(PlaceWordStatus.Invalid); } } for (int i = 0; i < wordPosition.Word.Length; i++) { char l = GetLetter(row, column + i); if (l == wordPosition.Word[i]) { // It's OK to already have a matching letter only if it only belongs to a crossing word (of opposite direction) foreach (WordPosition loop in GetWordPositionsFromSquare(row, column + i)) { if (loop.IsVertical == wordPosition.IsVertical) { return(PlaceWordStatus.Invalid); } } } else { // We need an empty cell for this letter if (l != '\0') { return(PlaceWordStatus.Invalid); } // We need a free cell above and below, or else we're too close if (IsOccupiedSquare(row - 1, column + i) || IsOccupiedSquare(row + 1, column + i)) { if (withTooClose) { result = PlaceWordStatus.TooClose; } else { return(PlaceWordStatus.Invalid); } } } } return(result); }
/// <summary>Find all the ways to add WordToPlace to placedWord, iterator returning an enumeration of matching WordPosition</summary> /// <param name="canonizedWordToPlace">Canonized form of Word to place (ex: NON·SEQUITUR)</param> /// <param name="originalWordToPlace">Original form of Word to place (ex: Non Sequitur)</param> /// <param name="placedWord">WordPosition to connect to</param> private IEnumerable <WordPosition> TryPlace(string canonizedWordToPlace, string originalWordToPlace, WordPosition placedWord) { // Build a dictionary of (letter, count) for each canonizedWord List <char> wordToPlaceLetters = BreakLetters(canonizedWordToPlace).Shuffle(); List <char> placedWordLetters = BreakLetters(placedWord.Word); // Internal helper function, returns a dictionary of (letter, count) for canonizedWord w List <char> BreakLetters(string w) { var set = new HashSet <char>(); foreach (char letter in w) { if (!set.Contains(letter)) { set.Add(letter); } } return(set.ToList()); } // For each letter of canonizedWordToPlace, look if placedWord contains this letter at least once foreach (char letter in wordToPlaceLetters) { if (placedWordLetters.Contains(letter)) { // Matching letter! // Helper, returns a list of positions of letter (external variable) in canonizedWord List <int> FindPositions(string word) { var list = new List <int>(3); int p = 0; for (; ;) { int q = word.IndexOf(letter, p); if (q < 0) { break; } list.Add(q); p = q + 1; } return(list); } // Look for all possible combinations of letter in both words if it's possible to place canonizedWordToPlace. foreach (int positionInWordToPlace in FindPositions(canonizedWordToPlace)) // positionsInWordToPlace) { foreach (int positionInPlacedWord in FindPositions(placedWord.Word)) // positionsInPlacedWord) { WordPosition test = new WordPosition(canonizedWordToPlace, originalWordToPlace, new PositionOrientation( placedWord.IsVertical ? placedWord.StartRow + positionInPlacedWord : placedWord.StartRow - positionInWordToPlace, placedWord.IsVertical ? placedWord.StartColumn - positionInWordToPlace : placedWord.StartColumn + positionInPlacedWord, !placedWord.IsVertical)); if (Layout.CanPlaceWord(test, false) == PlaceWordStatus.Valid) { yield return(test); } } } } } yield break; }
private IList <WordPosition> FindWordPossiblePlacements(string originalWord, PlaceWordOptimization optimization) { string canonizedWord = CanonizeWord(originalWord); // If it's the first canonizedWord of the layout, chose random orientation and place it at position (0, 0) if (Layout.WordPositionList.Count == 0) { WordPosition wordPosition = new WordPosition(canonizedWord, originalWord, new PositionOrientation(0, 0, rnd.NextDouble() > 0.5)); return(new List <WordPosition> { wordPosition }); } // Get current layout since we'll prefer placements that minimize layout extension to keep words grouped BoundingRectangle r = Layout.Bounds; int surface = ComputeAdjustedSurface(r.Max.Column - r.Min.Column + 1, r.Max.Row - r.Min.Row + 1); // Find first all positions where the canonizedWord can be added to current layout; List <WordPosition> possibleWordPositions = new List <WordPosition>(); List <WordPosition> possibleWordPositionsBelowThreshold = new List <WordPosition>(); int minSurface = int.MaxValue; foreach (WordPosition wordPosition in Layout.WordPositionList) { foreach (WordPosition placedWordPosition in TryPlace(canonizedWord, originalWord, wordPosition)) { BoundingRectangle newR = WordPositionLayout.ExtendBounds(r, placedWordPosition); if (newR.Equals(r)) { if (optimization == PlaceWordOptimization.Aggressive) { return new List <WordPosition> { placedWordPosition } } ; if (optimization == PlaceWordOptimization.High) { possibleWordPositionsBelowThreshold.Add(placedWordPosition); } } int newSurface = ComputeAdjustedSurface(newR.Max.Column - newR.Min.Column + 1, newR.Max.Row - newR.Min.Row + 1); possibleWordPositions.Add(placedWordPosition); switch (optimization) { case PlaceWordOptimization.Aggressive: case PlaceWordOptimization.High: if (possibleWordPositions.Count > 0 && minSurface > newSurface) { possibleWordPositions.RemoveAt(0); minSurface = newSurface; } if (possibleWordPositions.Count == 0) { possibleWordPositions.Add(placedWordPosition); } break; case PlaceWordOptimization.Standard: if (newSurface < surface * 1.15) { possibleWordPositionsBelowThreshold.Add(placedWordPosition); } possibleWordPositions.Add(placedWordPosition); break; default: possibleWordPositions.Add(placedWordPosition); break; } } } if (possibleWordPositionsBelowThreshold.Count > 0) { return(possibleWordPositionsBelowThreshold); } return(possibleWordPositions); }