joshbduncan / word-search-generator

Make awesome Word Search puzzles!
MIT License
71 stars 21 forks source link

Quick Questions - Solution, Diff Level and word lists. #57

Closed nicolesimon13 closed 6 months ago

nicolesimon13 commented 7 months ago

Hi Josh, thanks for making this, so far this has been my favorite ws tool! And the first real reason I have for installing python (I am more of awk kinda girl).

I have not worked much yet with the mask but it is awesome! :)

All the following questions are about accessing the tool from inside of python. The one answer I am really looking for is the first one. ;)

How to create the solution from inside python? Maybe I am blind but how do I create the solution sheet? I can see the CLI -c but not how I can call for that in python? Or do you mean by solution your direction thingy? That is helpful for many other things but a typical solution is the filled board without additional letters.

Difficulty Level is just directions? Also I was trying to figure out what determines a hard level versus an easy level? Is it just the direction list? (1)

Used / Unused Words Can you say a bit about how your algo about placing works?

We often want to produce a puzzle with a theme, not just a random word list. Lets say items from the kitchen.

But also always have f.e. 30 words in our 15x15 puzzle. The solution would be to have a 50 word list for the algo to choose from. It is my understanding that at the moment the code tries to place all the words in the submitted file? Is there a way to prepare the word list file to optimize the placement?

to tell it "use 30 words from this file until you have 30 words"? does the algo work better with longer / shorter words? Is there any reason to sort the word file by length?

Another reason I ask about the Algo are the 100 word challenges. For this it needs to be x words and knowing more about what works and what does not would help with this.

Other things I would like to do but dont know how to access them (again all inside, you already have these but I dont know how one would access them)

Additional options which probably are already there but I was missing them in the documentation

fiy (1) "You can find all of the preset numeric levels listed in config.py." https://github.com/joshbduncan/word-search-generator/wiki/Puzzle-Object is a dead link. I have yet to find where windows stores the py file so I appreciate it in the documentation.

https://github.com/joshbduncan/word-search-generator/wiki/Puzzle-Object uses this line $ word-search dog, cat, pig, horse -k > puzzle.txt but there is no -k in https://github.com/joshbduncan/word-search-generator/wiki/Command-Line-Interface-(CLI)

nicolesimon13 commented 7 months ago

Another idea - load puzzle from json file. This way one can generate a file and later add words to it or work with it again / fix mispelling etc.

joshbduncan commented 7 months ago

Hey, thanks for checking out the package. It is always nice to hear when someone is using it.

First, let me say I have a pretty large update coming that changes a lot of the core code. The functionality doesn't really change, the changes just make the package more extensible. There are changes in the word placement/generation code that cleans things up a bit, and you can also disable/enable word validators (and create your own).

Anyway, now on to your questions... There is a lot to cover so I'll probably tackle it in pieces as I have time.

How to create the solution from inside python?

The WordSearch object has a show method, that accepts two arguments solution and hide_fillers. If you set solution to True like below, the puzzle will be printed with the words highlighted in red (same as passing the -c flag to the cli).

puzzle= WordSearch("cat bat rat")
puzzle.show(solution=True)

If you set hide_fillers to True the puzzle will be output with only the words shown (like below). The solution argument is ignore if hide_fillers is set to True.

---------------------------
        WORD SEARCH        
---------------------------

      B                    
        A                  
          T                
  C A T                    

              R A T        

Find these words: BAT, CAT, RAT
* Words can go SE, E, S, and, NE.

Answer Key: BAT SE @ (4, 3), CAT E @ (2, 6), RAT E @ (8, 8)

Difficulty Level is just directions?

Yes. Originally it was called directions, but was changed to allow for future extensibilty. Bascially, the simple levels keep working right-reading and the higher levels allow for words to be backward. The current options are below.

level_dirs: dict[int, set[Direction]] = {
    -1: set(),  # no valid directions
    1: {  # right or down
        Direction.E,
        Direction.S,
    },
    2: {  # right-facing or down
        Direction.NE,
        Direction.E,
        Direction.SE,
        Direction.S,
    },
    3: {  # any direction
        Direction.N,
        Direction.NE,
        Direction.E,
        Direction.SE,
        Direction.S,
        Direction.SW,
        Direction.W,
        Direction.NW,
    },
    4: {  # no E or S for better hiding
        Direction.N,
        Direction.NE,
        Direction.SE,
        Direction.SW,
        Direction.W,
        Direction.NW,
    },
    5: {  # no E
        Direction.N,
        Direction.NE,
        Direction.SE,
        Direction.S,
        Direction.SW,
        Direction.W,
        Direction.NW,
    },
    7: {  # diagonals only
        Direction.NE,
        Direction.SE,
        Direction.NW,
        Direction.SW,
    },
    8: {  # no diagonals
        Direction.N,
        Direction.E,
        Direction.W,
        Direction.S,
    },
}

more answers to come...

joshbduncan commented 7 months ago

Used / Unused Words

There are some substantial changes coming in relation to puzzle words and puzzle generation, so I'll talk about them since the current (<v4.0.0) way will be no more. The changes in a nutshell are, that I have extracted most of the functionality out into separate units because I have had quite a few people wanting to build other "word-based" games with this package. Now, all games derive from a base Game object. Each Game can have custom a Generator for generating the puzzle, a Formatter for displaying/outputting the puzzle, and custom word Validators (e.g. no palindromes, no punctuation, no single letter words, no subwords) for validating puzzle words.

Previously, all word validation was done during the WordSearch object initialization (and also after making any changes to the puzzle words). Now, the default validation (no single letter words, no palindromes, no words that fit inside of other words or encase other words) has been abstracted away. Each validator is now based on a Validator() abstract base class, allowing users to create their own or disable the defaults. This thought has come up before but because of issue #45 I decided to tackle. Normally in a standard word search puzzle you don't want single-letter words, palindromes, or words that are part of other words, as each of these situations could potentially lead to multiple solutions for the same puzzle. Also, there has been a --no-validators flag added to the cli to disable default validators.

The key steps:

  1. First all supplied words are validated using the supplied/default validators. Some things to note: Puzzle words are stored in a set so their order can't be guaranteed. They are processed in the order they are supplied but their order may change for numerous reasons.
  2. The specified Generator starts by trying to place all "regular hidden" (not secret) words in the puzzle. To do this, it picks a random empty starting position on the puzzle and checks if the word can actually fit there going in any of the available directions (level). If so, it picks a random valid direction, and starts placing the word character by character. For every character, the generator checks to make sure that by placing a particular character another word isn't created by coincidence. If all checks pass the word is placed. If not, it tries another valid direction. After it exhausts all valid directions at the random position, a WordFitError is raised and it tries everything again at another random position. This retry continues up to 1000 times. If no feasible position can be found by then the generator skips that word and moves on to the next.
  3. After the generator tries all regular "hidden" words it then does the same for "secret" words.
  4. Then after all words have been looped over, the generator fills in the blank spots with random characters. The same duplicate word "coincidence" check is run for every placed random character.

I'll continue on later when I have more time... And sorry in advance, I am typing this on my phone...

joshbduncan commented 7 months ago

Used / Unused Words (contd.)

If you are wanting to ensure 30 words inside of a size 15 (15x15) puzzle, you could use a simple while loop like below...

from word_search_generator import WordSearch

words = "oven microwave fridge knife fork..."  # string of 30 words
puzzle = WordSearch(words, size=15)

if puzzle.unplaced_hidden_words:
    while puzzle.unplaced_hidden_words:
        puzzle._generate()

The generator is pretty good at filling up a puzzle but 30 words in a 15x15 puzzle is a tight fit. Sometime it will fit them all but sometimes it won't so the while loop keeps running the generator until all 30 words are successfully placed.

When trying to fit 100 words (or any number) the generator does a pretty good job of calculating a good size that allows all words to be fit (without being too bunched up). It not 100% but it is pretty good.

If you need to ensure you have exactly 100 words, you could use the same while loop code as above. You would just need to either not set a puzzle size and make it larger than 15.

joshbduncan commented 7 months ago

print out just the puzzle / the words used / answer key / direction list

You can print a generated puzzle using either print or the show method.

puzzle= WordSearch("cat bat rat")
print(puzzle)
# or
puzzle.show()

access the array for each of those items

puzzle= WordSearch("cat bat rat")

# puzzle
puzzle.puzzle

# words used
puzzle.placed_words

# directions
puzzle.directions # or p.level

# key
puzzle.key

print out the puzzle but just the words for the solution

puzzle= WordSearch("cat bat rat")
puzzle.show(hide_fillers=True)

print out information for a mask. if I provide a mask, how may words will fit well into this mask?

This is not something that is implemented. You can preview the mask using the show method.

def show(self, active_only: bool = False) -> None:
    """Pretty print the mask. When `active_only` is True only the masked
    areas that lie within the bound of (0,0) and (`puzzle_size`, `puzzle_size`)
    will be shown. Used for mask creation and testing.

    Args:
        active_only (bool, optional): Only output the masked areas
            that lie within the bounds of `Mask.puzzle_size`. Used for
            mask creation and testing. Defaults to False.

    Raises:
        MaskNotGenerated: Mask has not yet been generated.
    """
    ...

create the puzzle but fill it with something else for missing letters (f.e. a space or a colored block) why? f.e. the number puzzle below, it makes more sense to block out remaining spaces to avoid duplication problems)

I assume you are looking to make crossword style puzzles. This is one of the main reasons while I did all of the extraction I mentioned above (quite a few people have requested crossword puzzles). The best way to go about this would be to write a new custom Crossword generator (and formatter as well). It is something I plan to do in the future, I jsut don't have a timeline for working on it.

what is this cli option? --no-validators Disable default word validators.

This is a new feature (I described in detail in a previous comment) that disables all default word validators for the WordSearch game.

joshbduncan commented 7 months ago

make a puzzle reverse word search.

Not implemented but could be created pretty easily with a custom generator and formatter.

make a number word search puzzle

Not implemented but could be created pretty easily with a custom generator and formatter. Would honeslty be a bit hacky, since the base game is designed to work with words and letters but could defintely be done.

create 'crosswords'

Not implemented but planned as I mentioned in a previous comment.

joshbduncan commented 7 months ago

Another idea - load puzzle from json file.

This is on my to-do list as it has been requested before. The only thing to note, any adjustments to words will generate an entirely different looing puzzle (meaning words will be in different places).

joshbduncan commented 7 months ago

config.py

Link has been updated in the wiki. You can view config.py using this link as well.

cli redirection example

Fixed. Not sure where the -k came from. You don't need any flags to redirect the puzzle output to a text file.

I think that answers everything you asked. If you have more questions or need help with anything, don't hesitate to ask. Cheers!

nicolesimon13 commented 7 months ago

thank you for all your answers, will have to work through them!

Another idea - load puzzle from json file.

This is on my to-do list as it has been requested before. The only thing to note, any adjustments to words will generate an entirely different looing puzzle (meaning words will be in different places). I only had a quick look and I thought the json file had the whole puzzle in it?

This retry continues up to 1000 times. If no feasible position can be found by then the generator skips that word and moves on to the next. I have another tool which uses 100k or 200k retries. I am willing to let the thing run in the background for a while. I am not that good at math but based on the remaining empty spots and size of the grid should there not a determined limited number of tries?

And sorry in advance, I am typing this on my phone... people are crazy doing that on the phone! :)

You mentioned the update - any idea when that might be coming? 1 month, 6 months? ;) Also there should not be that many changes for the basic word search generation, right?

joshbduncan commented 7 months ago

Just to clarify what I meant by "The only thing to note, any adjustments to words will generate an entirely different looking puzzle (meaning words will be in different places)."

Anytime a change is made to a puzzle object, add words, remove words, change size, add mask, etc., the puzzle gets regenerated. As you can see below, the simple puzzle completely changes when I add a new word.

from word_search_generator import WordSearch

p = WordSearch("cat dog ant", size=5)
p.show()
# -----------
# WORD SEARCH
# -----------
#  X A C F K
#  D I U A C
#  R D O G T
#  E A N T W
#  S F J B F
#
# Find these words: ANT, CAT, DOG
# * Words can go SE, E, S, and, NE.
#
# Answer Key: ANT E @ (2, 4), CAT SE @ (3, 1), DOG E @ (2, 3)

p.add_words("rat")
p.show()
# -----------
# WORD SEARCH
# -----------
#  P I C A T
#  B G G N R
#  N O I T A
#  D E B S T
#  R U W M N
# 
# Find these words: ANT, CAT, DOG, RAT
# * Words can go SE, E, S, and, NE.
# 
# Answer Key: ANT S @ (4, 1), CAT E @ (3, 1), DOG NE @ (1, 4), RAT S @ (5, 2)

So if you saved out a JSON representation of a puzzle, and then loaded it back in to change a word (like your example) the puzzle "layout" would be regenerated and words would be in different places.

Full puzzle output in JSON

The full puzzle is given in the via the JSON method as seen below.

p.json
{
  "puzzle": [
    ["P", "I", "C", "A", "T"],
    ["B", "G", "G", "N", "R"],
    ["N", "O", "I", "T", "A"],
    ["D", "E", "B", "S", "T"],
    ["R", "U", "W", "M", "N"]
  ],
  "words": ["RAT", "CAT", "ANT", "DOG"],
  "key": {
    "DOG": {
      "start_row": 3,
      "start_col": 0,
      "direction": "NE",
      "secret": false
    },
    "ANT": {
      "start_row": 0,
      "start_col": 3,
      "direction": "S",
      "secret": false
    },
    "CAT": {
      "start_row": 0,
      "start_col": 2,
      "direction": "E",
      "secret": false
    },
    "RAT": {
      "start_row": 1,
      "start_col": 4,
      "direction": "S",
      "secret": false
    }
  }
}

1000 retries limit

This limit is actually pretty high (probably higher than it needs to be). Rarely is this limit hit unless the word just won't fit into the current puzzle. I did some testing and with keeping speed and efficiency in mind, any higher limit just didn't make sense. However, that is the good thing about the new extraction layout of the package. You could easily write a new generator with a much higher limit if you need it.

v4.0.0 Update Timing

I'm pretty sure I'll push the update live in the next few weeks. I have a few other things I want to add but nothing that should take a lot of time. And the function of the word search generation is the same as no changes were made there.