Giter Club home page Giter Club logo

Comments (20)

joshbduncan avatar joshbduncan commented on June 16, 2024 1

Yes, I agree about the CLI. Maybe it's something we can implement later but I think it's best to just get everything working in the API.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024 1

@duck57, I like masks. I feel like more people would understand that over filer.

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

Before I do any more work on this, what do you think of my Puzzle object and approach to the modification functions? My gut feeling suggests that there is some far easier way to wrap a list[list[chr]] into an object and handle all those modification functions. However, I'm drawing a blank as to what those changes may be.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

Hey, I was out of town last weekend so I haven't had the time to review this. I definitely like the idea so I'll try to take a look at the implementation this weekend and get back to you. Thanks!

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

This is a bit of a thought dump for when I go to do the implementation. Feel free to ignore this if you don't find it useful when giving my demo code an in-depth review.

Data Structure of Puzzle

For 90% of what I want to do, list[list[str]] is all I need. However, there are some instances when I think treating the Puzzle as dict[Position, str] (a.k.a. dict[tuple[int, int], str]) would be more convenient.

Properties or just additional tasks for the setter?

I highly doubt this would become a performance-sensitive issue. However, I'm unfamiliar with how Python caches its @properties—would they need to be recomputed every time they're accessed or just each time they've been accessed after the Puzzle has changed?

  • Translation to dict or list[list] form from the "true" internal representation
  • by_chr() -> dict[chr, set[Position]]: List out all the cells containing the character, used for…
  • dead_cells() -> set[Position]: set of cells masked out of the board (but within its nominal boundaries)
  • empty_cells() -> set [Position]: same as above but for fully-empty cells
  • When implementing this, alter fit_word to choose starting positions from something like random.choice(empty_cells | by_chr()[word[0]]).

Structure of filter functions

I'm going to change up the signature of these functions. Namely, instead of some ad-hoc boolean about whether to blackout or clear the selection, separate out the selections and effects into separate functions. Remaining to be decided: does the selection function take an effect function as a param or do I make the filter list accept tuples, making it list[tuple[selection, effect]]?

Typing this out, probably best to use the effect function as a param in the pattern function.

Below are the four effect functions I've thought of.

def mark_oob(_: chr) -> chr:
    return config.OOB_CHR

def mark_clear(_: chr) -> chr:
    return ""

def toggle_cell(c: chr) -> chr:
    return "" if c == config.OOB_CHR else config.OOB_CHR

def random(c: chr, effect: Optional[Callable[[chr], chr]] = None, strength: float = 0.5) -> chr:
    if not effect:
        effect = random.choice([mark_oob, mark_clear, toggle_cell])
    return effect(c) if random.random() < strength else c
  1. Do I make these (or the selection functions) functions or methods of a Puzzle instance?
  2. I'm going to have to get familiar with functools.partial if I don't want to use nested functions, won't I?

List of patterns to implement:

  • Whole board (currently implemented as blackout)
  • Triangle (specify a Direction to anchor its corner)
  • Pick cell
  • Circle/ellipse
  • rectangular punch

There were probably some other items, but I started typing this over my lunch break on Friday and then returned to it after work this evening, so there's plenty I've forgotten.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

@duck57, if we are going to put in the work to build this out I want to clean up a few things to make the module a bit more robust before we start.

So far, I've already done the work to create a word class that tracks the text, position, coordinates (for potential use), xy position for display. So far, it's def better and just keeping a set of words and a separate key. I just need to finish implementing this into the function.

I'm also cleaning up the utility and generate function to use the actual puzzle object so that we don't have to pass around so many variables. I just pass the puzzle object and extract the variable I need to do the work from there.

Next, I'm going to clean up the actual puzzle generator function. I bolted on checks as I implemented them so I need to go back in and clean things up.

I hope to have this all done in the next few days.

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

I see you've made some of the changes already. I've done similar refactors (RE: moving things to a class because of excessively redundant function signatures) on my own projects before.

Is the plan to finish the cleanup and then merge #17? If so, I'll wait until after that has been merged to explore more so there will be a stable foundation to build from.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

Exactly! I've got the work done for creating the word class object and will commit it tomorrow. I think that's the last thing I'm going to squeeze into that PR. I wanted to have a more solid foundation and easier extensibility before we work to add these advanced features. It would be even harder to make the switch later.

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

For the sake of consistent nomenclature, should this feature be called "masks" or "filters"?

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024
  1. Do I make these (or the selection functions) functions or methods of a Puzzle instance?

I would make them methods of the Puzzle object since they only interact with a "puzzle".

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

2. I'm going to have to get familiar with functools.partial if I don't want to use nested functions, won't I?

I've never actually used functools.partial in a working program ( just doesn't come to mind)... But yes, it could certainly help to reduce nesting and clean up a function.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

So, on your proof of concept, I see that masks are applied by expanding the puzzle. What is your plan for making that work with puzzle size? Say I supply a puzzle size of 10h x 20w like below with some random masks.

>>> p = Puzzle(
    10,
    20,
    masks=[
        expand(0, 4, True),
        expand(0, -7, True),
        expand(-3, 0, False),
        expand(3, 0, False),
        expand(0, 2, False),
        expand(0, -2, False),
    ],
)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . # # # # # # # . . . . . . . . . . . . . . . . . . . . # # # # . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

If I now call p.width or p.height the values don't match what I originally set.

>>> p.height
16
>>> p.width
35
>>> print(p)

I understand what is happening but do you think the user will? Should we "remove" the masked areas from the actual puzzle size they specify like below?

 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #
 # # # # # # # . . . . . . . . . # # # #

Your original "I" shaped example wouldn't work in this case though as you are masking out 8 cols and the puzzle size is only 6 cols. This could throw the exception you have set up. But if you reduced the masks size to 2 cols and could control the height you would end up with the following...

>>> print(
    Puzzle(
        6,
        masks=[
            remove_rectangle(row=2, col=0, width=2, height=2, True),
            remove_rectangle(row=2, col=4, width=2, height=2, True),
        ],
    )
)
. . . . . .
. . . . . .
# # . . # #
# # . . # #
. . . . . .
. . . . . .

Just some thoughts as I try and wrap my head around the implementation...

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

And I don't think it would be too hard to implement a bitmap mask using PIL...

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

@duck57, FYI, I had to patch a small booboo so I just published v2.0.1. My check for all of the placed words properties when checking to see if the word had a 'position'. Well the way we have position setup it always return true making all words show up in those properties and the key no matter if they were placed on the board or not. Was easy to notice when I tried to fit 100 words in a 5x5 puzzle. 🤦‍♂️

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

I just pushed a commit to my fork's mask-test branch. It's very much so a WIP. As I said in the commit message, truly a minimal viable proof-of-concept for the changes. However, I probably won't have time to do in-depth work on it again until mid-February.

This commit (or series of commits, rather):

  1. Merges in #17 to use it as the starting base [I started this on Tuesday, so it's before you've made your comments]
  2. Re-implements a few masks to work on a list[list[str]] instead of a Puzzle object—the Puzzle object seemed like it would end up being a replacement for the WordSearch object if it kept growing.
  3. Leaves puzzle.py mostly alone. The re-implementations are in masks.py
  4. At least adds some support for polling which cells contain which character (though I'd still need to write tests to see if it works/is used appropriately)
  5. The new mask implementation has been hooked up with WordSearch objects. They do correctly build the word searches around the fenced-off coordinates.
  6. Allow for the creation of WordSearches with width≠height

Some to-do items [this list is as much for me as for you]:

  • Increase the number of masks. Notable absences in the current set are the triangle (built from a corner), circle/ellipse, and clear_column/row.
  • Provide a proper demo of constructing a WordSearch using masks. For one, I want to make a heart shape with a circle and triangle that gets mirrored.
  • Documentation!!!! If you review all this (or when I come back in the spring), I hope I remember how this all fits together.
  • Pick which implementation to build from. They're quite similar, puzzle.py and masks.py.
  • Alter the find_a_fit function to randomly choose from cells that are either empty or contain the starting letter of the word instead of choosing randomly from any cell on the grid.
  • Write tests to make sure it actually works

To address some of your comments,

  • At least for PyCharm's type-checker, it likes the nested functions more than it likes a Partial object. I wonder if there are any performance implications between nested functions and Partials.
  • Would we want to bring in PIL as a dependency or should we have some code that will accept a bitmap mask, but specify in the documentation that anyone who wants to use this would have to import PIL themself?
  • When I made my take-two implementation, I kept the behavior of altering the size for the expand and chop functions. I'd hope that the names alone would make it clear enough that these functions would alter the size of the grid. Perhaps the mirror functions in masks.py should be renamed to something like grow_mirror

If I have unexpected free time this weekend or early next week, I may take a stab at addressing some of the to-do items. If not, it can be a project for me in the spring. Feel free to use my chicken scratch as the base for your implementation if you want if you don't want to wait until later February.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

@duck57, so after talking it over with a few friends, I wanted to take a go at creating a different approach for masking. I'm a graphic designer by trade so masks are something I use a lot and this approach just works better with my brain. It needs some cleanup and has zero type hints at the moment but it should be pretty easy to understand. I have included a pretty extensive readme below.

I plan to add more shapes, probably recalculate the heart, and check on some rounding issues, but most of the base is there. The bitmap mask could easily be expanded to work with PIL (which is already a requirement of the PDF generator) and allow user images to work as masks.

This could pretty easily be implemented into the actual package.

I'm on my lunch break and rambling at this point, so check it out if you have time, and let me know what you think.

https://gist.github.com/joshbduncan/949caf7a6d0ae6d9d2ada564a0562f4f

Puzzle Masks

Masks allow you to "mask" areas of a WordSearch puzzle, making those areas inactive for placing characters.

Mask() Base Class

All puzzle masks are based on the base Mask class. There are two subclasses, Bitmap and Polygon, that inherits from Mask.

def __init__(self, method=1, static=True):
    """A puzzle mask object.

    Args:
        method (int, optional): Masking method. Defaults to 1.
            1. Standard (Intersection)
            2. Additive
            3. Subtractive
        static (bool, optional): Mask should not be recalculated
        and reapplied after a size change. Defaults to True.
    """
    self.puzzle_size = None
    self.grid = None
    self.method = method
    self.static = static

The base mask class only has a few key properties, Mask.method and Mask.static.

Mask().method

Mask.method determines how the mask is applied to the puzzle.

To best understand Mask.method, let me show what a sample mask looks like.

>>> mask = Diamond()
>>> mask.generate(11)
>>> mask.show()
# # # # # * # # # # #
# # # # * * * # # # #
# # # * * * * * # # #
# # * * * * * * * # #
# * * * * * * * * * #
* * * * * * * * * * *
# * * * * * * * * * #
# # * * * * * * * # #
# # # * * * * * # # #
# # # # * * * # # # #
# # # # # * # # # # #

As you can see in the output above, a mask (no matter the type) is made up of ACTIVE (*) and INACTIVE (#) spaces. ACTIVE (*) spaces will "act" on a puzzle depending on the Mask.method. If it helps, in the physical world, the above mask would be a square with a diamond shape cut out of the middle, masking all of the areas marked INACTIVE (#), and revealing all of the areas marked ACTIVE (*).

Method Types

  1. Standard: All INACTIVE (#) spaces from the mask will deactivate corresponding spaces on the current puzzle, intersecting with any previously applied masks.
>>> p = Puzzle(15)
>>> p.apply_mask(Ellipse(15, 7))
>>> p.show()
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # * * * * * * * # # # #
# * * * * * * * * * * * * * #
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
# * * * * * * * * * * * * * #
# # # # * * * * * * * # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
>>> p.apply_mask(Ellipse(7, 15, method=2))
>>> p.show()
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # * * * * * * * # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #

Using the default standard method (method=1) on the second vertical oval mask, you can see that it interacts with the previous mask so only intersecting/overlapping areas are active on the puzzle.

  1. Additive: All ACTIVE (*) spaces from the mask will activate corresponding spaces on the current puzzle, no matter the current puzzle state.
>>> p = Puzzle(15)
>>> p.apply_mask(Ellipse(15, 7))
>>> p.show()
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # * * * * * * * # # # #
# * * * * * * * * * * * * * #
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
# * * * * * * * * * * * * * #
# # # # * * * * * * * # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
>>> p.apply_mask(Ellipse(7, 15, method=2))
>>> p.show()
# # # # # # * * * # # # # # #
# # # # # * * * * * # # # # #
# # # # # * * * * * # # # # #
# # # # # * * * * * # # # # #
# # # # * * * * * * * # # # #
# * * * * * * * * * * * * * #
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
# * * * * * * * * * * * * * #
# # # # * * * * * * * # # # #
# # # # # * * * * * # # # # #
# # # # # * * * * * # # # # #
# # # # # * * * * * # # # # #
# # # # # # * * * # # # # # #

Using the additive method (method=2) on the second vertical oval mask, you can see that it doesn't interact with the previous mask at all and simply activates all of it's area on the current puzzle.

  1. Subtractive: All ACTIVE (*) spaces from the mask will deactivate corresponding spaces on the current puzzle, no matter the current puzzle state.
>>> p = Puzzle(15)
>>> p.apply_mask(Ellipse(15, 7))
>>> p.show()
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # * * * * * * * # # # #
# * * * * * * * * * * * * * #
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
* * * * * * * * * * * * * * *
# * * * * * * * * * * * * * #
# # # # * * * * * * * # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
>>> p.apply_mask(Ellipse(7, 15, method=3))
>>> p.show()
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# * * * # # # # # # # * * * #
* * * * # # # # # # # * * * *
* * * * # # # # # # # * * * *
* * * * # # # # # # # * * * *
# * * * # # # # # # # * * * #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # # # # # #

Using the subtractive method (method=3) on the second vertical oval mask, you can see that it doesn't interact with the previous mask at all and simply deactivates all of its area on the current puzzle.

Mask().static

A Puzzle object retains all applied masks so that they can be reapplied if the puzzle size changes. Only masks marked as non static Mask.static = False will be reapplied. All masks are marked as True by default.

The reason for this property, is there are many Preset Masks that are calculated based on the puzzle size. These masks will easily scale if you change the puzzle size. But a problem arises when you create a custom Bitmap or Polygon mask that can't be easily recalculated to fit on a different puzzle size. In this case (Mask.static = True) the mask will remain in Puzzle.masks but will not be re-applied when the puzzle size changes.

If you would like to remove all static masks from Puzzle.masks after a resize, you can use Puzzle.remove_static_masks(). If you want to remove all masks from a puzzle (static or not), use Puzzle.remove_masks()

Masks can be applied to a Puzzle object using Puzzle.apply_mask() (for singular operations) or Puzzle.apply_mask([List]) (for multiple operations).

Mask() Methods

  • Mask.generate() will generate a mask at the supplied puzzle size (required). If no points have been specified the mask will be solid.
  • Mask.show() will show a visual representation of the mask. Mostly for creating and testing.
  • Mask.flip_horizontal() will flip the entire mask horizontally (left to right).
  • Mask.flip_vertical() will flip the entire mask vertically (top to bottom).
  • Mask.transpose() will interchange each row with the corresponding column of the mask.

Mask Shape Centering

Please note, anytime a mask shape with a calculated center (Triangle, Diamond, Ellipse, Star, Heart) is applied to a puzzle with an even Puzzle.size the mask will be offset one grid unit toward the top-left origin point (0, 0) since there is no true center.

Puzzle size is even and an Ellipse size is odd...

>>> p = Puzzle(9)
>>> p.apply_mask(Ellipse(8, 4))
>>> p.show()
# # # # # # # # #
# # # # # # # # #
# * * * * * * # #
* * * * * * * * #
* * * * * * * * #
# * * * * * * # #
# # # # # # # # #
# # # # # # # # #
# # # # # # # # #

Puzzle size is odd and an Ellipse size is even...

>>> p = Puzzle(10)
>>> p.apply_mask(Ellipse(9, 5))
>>> p.show()
# # # # # # # # # #
# # # # # # # # # #
# # * * * * * # # #
* * * * * * * * * #
* * * * * * * * * #
* * * * * * * * * #
# # * * * * * # # #
# # # # # # # # # #
# # # # # # # # # #
# # # # # # # # # #

Preset Masks

Current preset masks:

  • Bitmap Masks

    • Ellipse
    • Circle
  • Polygon Mask

    • Triangle
    • Diamond
    • Pentagon
    • Hexagon
    • Octagon
    • Star
    • Heart

Bitmap Masks

Bitmap masks work similarly to bitmap images. Every point (grid square) specified in the Bitmap.points property will be included in the mask.

Masks that inherit from Bitmap:

Ellipse

Draw an ellipse at the specified width and height on the puzzle. This is the mask type that was used above to explain Mask Method Types.

>>> p = Puzzle(20)
>>> p.apply_mask(Ellipse(18,10))
>>> p.show()
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # * * * * * * * * # # # # # #
# # # # * * * * * * * * * * * * # # # #
# # * * * * * * * * * * * * * * * * # #
# * * * * * * * * * * * * * * * * * * #
# * * * * * * * * * * * * * * * * * * #
# * * * * * * * * * * * * * * * * * * #
# * * * * * * * * * * * * * * * * * * #
# # * * * * * * * * * * * * * * * * # #
# # # # * * * * * * * * * * * * # # # #
# # # # # # * * * * * * * * # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #

Circle

Draw a circle that fills the entire puzzle. And, since a circle is just an ellipse with width == height, the Circle class inherits from the Ellipse class but doesn't accept any parameters.

>>> p = Puzzle(10)
>>> p.apply_mask(Circle())
>>> p.show()
# # # * * * * # # #
# * * * * * * * * #
# * * * * * * * * #
* * * * * * * * * *
* * * * * * * * * *
* * * * * * * * * *
* * * * * * * * * *
# * * * * * * * * #
# * * * * * * * * #
# # # * * * * # # #

🍩 Donuts anyone?

>>> p = Puzzle(21)
>>> e1 = Ellipse(21, 21)
>>> e2 = Ellipse(9, 9, method=3)
>>> p.apply_masks([e1, e2])
>>> p.show()
# # # # # # # * * * * * * * # # # # # # #
# # # # # * * * * * * * * * * * # # # # #
# # # # * * * * * * * * * * * * * # # # #
# # # * * * * * * * * * * * * * * * # # #
# # * * * * * * * * * * * * * * * * * # #
# * * * * * * * * * * * * * * * * * * * #
# * * * * * * * # # # # # * * * * * * * #
* * * * * * * # # # # # # # * * * * * * *
* * * * * * # # # # # # # # # * * * * * *
* * * * * * # # # # # # # # # * * * * * *
* * * * * * # # # # # # # # # * * * * * *
* * * * * * # # # # # # # # # * * * * * *
* * * * * * # # # # # # # # # * * * * * *
* * * * * * * # # # # # # # * * * * * * *
# * * * * * * * # # # # # * * * * * * * #
# * * * * * * * * * * * * * * * * * * * #
# # * * * * * * * * * * * * * * * * * # #
# # # * * * * * * * * * * * * * * * # # #
# # # # * * * * * * * * * * * * * # # # #
# # # # # * * * * * * * * * * * # # # # #
# # # # # # # * * * * * * * # # # # # # #

Polygon Masks

Polygon masks accept a list of at least 3 points. During mask generation those points will be connected using the Bresenham's line algorithm, then the shape will be filled using the Polygon.fill_shape() method.

>>> p = Puzzle(11)
>>> polygon = Polygon([(1,1), (7,4), (2,9)])
>>> p.apply_mask(polygon)
# # # # # # # # # # #
# * * # # # # # # # #
# * * * * # # # # # #
# * * * * * * # # # #
# * * * * * * * # # #
# # * * * * * # # # #
# # * * * * # # # # #
# # * * * # # # # # #
# # * * # # # # # # #
# # * # # # # # # # #
# # # # # # # # # # #

⚠️ I have noticed that on some polygons Bresenham's line algorithm calculation can be slightly off when drawing back toward the origin point (after the halfway point). This may be due to rounding but I am not sure at this point. I have created a fix (not yet implemented) that draws the first half of the path from the origin, then returns to the origin and draws the second half of the path in reverse order. I just need to iron out a few edge cases before implementing it.

Masks that inherit from Polygon

Rectangle

Draw a rectangle mask from 4 points. The points should be specified as a list of (x, y) tuples. The default origin point of (0, 0) is at the top-left of the puzzle.

>>> p = Puzzle(11)
>>> p.apply_mask(Rectangle(5,7))
>>> p.show()
* * * * * * * # # # #
* * * * * * * # # # #
* * * * * * * # # # #
* * * * * * * # # # #
* * * * * * * # # # #
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #

You can also specify a specific (x, y) origin position=(2,3) from where the rectangle will be drawn.

>>> p = Puzzle(11)
>>> p.apply_mask(Rectangle(5,7, position=(3,4)))
>>> p.show()
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #
# # # # * * * * * * *
# # # # * * * * * * *
# # # # * * * * * * *
# # # # * * * * * * *
# # # # * * * * * * *
# # # # # # # # # # #
# # # # # # # # # # #
# # # # # # # # # # #

Triangle

Draw a triangle that fills the entire puzzle.

  • An odd puzzle_size will generate an Isosceles Triangle.
  • An even puzzle_size will generate a Scalene Triangle.

⚠️ If you prefer an Equilateral Triangle, don't worry that is built-in too using the ConvexPolygon mask so no need to worry with the calculation.

>>> p = Puzzle(11)
>>> p.apply_mask(Triangle())
>>> p.show()
# # # # # * # # # # #
# # # # # * * # # # #
# # # # * * * # # # #
# # # # * * * * # # #
# # # * * * * * # # #
# # # * * * * * * # #
# # * * * * * * * # #
# # * * * * * * * * #
# * * * * * * * * * #
# * * * * * * * * * *
* * * * * * * * * * *

Diamond

Draw a diamond that fills the entire puzzle.

  • An odd puzzle_size will generate a Rhombus with equal sides.
>>> p = Puzzle(10)
>>> p.apply_mask(Diamond())
>>> p.show()
# # # # * # # # # #
# # # * * * # # # #
# # * * * * * * # #
# * * * * * * * * #
* * * * * * * * * *
# * * * * * * * * #
# # * * * * * * # #
# # * * * * * # # #
# # # * * * # # # #
# # # # * # # # # #
  • An even puzzle_size will generate a Trapezoid with unequal sides.
>>> p = Puzzle(11)
>>> p.apply_mask(Diamond())
>>> p.show()
# # # # # * # # # # #
# # # # * * * # # # #
# # # * * * * * # # #
# # * * * * * * * # #
# * * * * * * * * * #
* * * * * * * * * * *
# * * * * * * * * * #
# # * * * * * * * # #
# # # * * * * * # # #
# # # # * * * # # # #
# # # # # * # # # # #

If you want to generate an Equilateral Diamond (equal sides, with opposing sides parallel to each other) no matter the puzzle_size, there is a pre-built EquilateralDiamond mask based on calculations from the ConvexPolygon mask it inherits from.

Star

The Star or Pentagram is regular is a regular 5-pointed star polygon. The points for the star are calculated using the calculate_regular_convex_polygon_points() function just like the Pentagon below. The points are then rearranged and connected just like any Polygon mask.

>>> p = Puzzle(13)
>>> p.apply_mask(Heart())
>>> p.show()
# # # # # # * # # # # # #
# # # # # # * # # # # # #
# # # # # * * * # # # # #
# # # # # * * * # # # # #
* * * * * * * * * * * * *
# * * * * * * * * * * * #
# # # * * * * * * * # # #
# # # * * * * * * * # # #
# # # * * * * * * * # # #
# # # * * * # * * * # # #
# # * * * # # # * * * # #
# # * # # # # # # # * # #
# # # # # # # # # # # # #

The star will fill as much of the puzzle as possible and can be rotated.

>>> p = Puzzle(13)
>>> p.apply_mask(Heart(rotation=30))
>>> p.show()
# # # # # # # # # # # # #
# # # * # # # # # # # # #
# # # * * # # # # * * # #
# # # * * * # * * * # # #
# # # # * * * * * * # # #
# # # * * * * * * # # # #
# * * * * * * * * * # # #
* * * * * * * * * * * # #
# # # # * * * * * * * * #
# # # # * * * # # # # # #
# # # # # * * # # # # # #
# # # # # * # # # # # # #
# # # # # * # # # # # # #

Heart

>>> p = Puzzle(13)
>>> p.apply_mask(Heart())
>>> p.show()
# # * * # # # # # * * # #
# * * * * # # # * * * * #
# * * * * * # * * * * * #
* * * * * * * * * * * * *
* * * * * * * * * * * * *
* * * * * * * * * * * * *
* * * * * * * * * * * * *
# * * * * * * * * * * * #
# # * * * * * * * * * # #
# # # * * * * * * * # # #
# # # # * * * * * # # # #
# # # # # * * * # # # # #
# # # # # # * # # # # # #

ConvexPolygon

Draw a regular Convex Polygon mask with 3 or more sides. All points are calculated from the puzzle center and cover as much of the available puzzle area as possible.

All ConvexPolygon masks accept two parameters, sides and rotation.

Masks that inherit from ConvexPolygon:

* Are calculated as Regular Polygons

EquilateralTriangle

A Triangle in which all 3 sides have the same length and all three internal angles are also congruent to each other at 60°.

# # # # # # * # # # # # #
# # # # # * * * # # # # #
# # # # # * * * # # # # #
# # # # * * * * * # # # #
# # # # * * * * * # # # #
# # # * * * * * * * # # #
# # # * * * * * * * # # #
# # * * * * * * * * * # #
# # * * * * * * * * * # #
# * * * * * * * * * * * #
# # # # # # # # # # # # #
# # # # # # # # # # # # #
# # # # # # # # # # # # #

♺ Want it rotated?

>>> p = Puzzle(13)
>>> p.apply_mask(EquilateralTriangle(45))
>>> p.show()
# # # # # # # # # # # # #
# # # # # # # # # # # # #
# # * * * # # # # # # # #
# # * * * * * * * * # # #
# # * * * * * * * * * * *
# # # * * * * * * * * * #
# # # * * * * * * * * # #
# # # * * * * * * * # # #
# # # * * * * * * # # # #
# # # * * * * * # # # # #
# # # # * * * # # # # # #
# # # # * * # # # # # # #
# # # # * # # # # # # # #

EquilateralDiamond

A Diamond (rotated square) with 4 equal sides.

# # # # # # * # # # # # #
# # # # # * * * # # # # #
# # # # * * * * * # # # #
# # # * * * * * * * # # #
# # * * * * * * * * * # #
# * * * * * * * * * * * #
* * * * * * * * * * * * *
# * * * * * * * * * * * #
# # * * * * * * * * * # #
# # # * * * * * * * # # #
# # # # * * * * * # # # #
# # # # # * * * # # # # #
# # # # # # * # # # # # #

Pentagon

A simple Pentagon with 5 equal sides.

# # # # # # * # # # # # #
# # # # * * * * * # # # #
# # # * * * * * * * # # #
# * * * * * * * * * * * #
* * * * * * * * * * * * *
* * * * * * * * * * * * *
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# # * * * * * * * * * # #
# # * * * * * * * * * # #
# # # # # # # # # # # # #

Hexagon

A [simple Hexagon]

# # # # # # * # # # # # #
# # # # * * * * * # # # #
# # * * * * * * * * * # #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * #
# # * * * * * * * * * # #
# # # # * * * * * # # # #
# # # # # # * # # # # # #

Octagon

# # # # # * * # # # # # #
# # # * * * * * * # # # #
# # * * * * * * * * * # #
# # * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * *
* * * * * * * * * * * * *
* * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * # #
# # * * * * * * * * * # #
# # # # * * * * * * # # #
# # # # # # * * # # # # #

⚠️ If you want the Octagon to look like a stop sign you can set rotation=30 at creation.

❗️ Please note, there seems to be an off-by-1 miscalculation with the Octagon. It could be a rounding issue but I'm not sure at the moment. Hopefully, I can sort it out later.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

Some Fun Puzzle Masks

I had a few free minutes tonight so I made some fun mask shapes. It's really easy to make these. I could easily create a library of them.

Donut 🍩

>>> p = Puzzle(21)
>>> p.apply_mask(Ellipse(21, 21))
>>> p.apply_mask(Ellipse(9, 9, method=3))

              S W X O M K B
          T C G B J G K B R G Y
        Q C O Q T M O H B W R T L
      M C B G X K L G D Z K X O M F
    H O C T W Q F H Q J R U G Z F L H
  T R W D R V R B B R B F E V E J B O U
  T A J U W B Z           L J M F B A I
C E G U H U L               F O I T K P R
X U O A F A                   F F K G Y Y
L M O R I U                   K T W Z J H
V F Y N J V                   B N A D B V
W Y Y G H E                   C A L Z T S
R R R P Y N                   H H S G Z Z
L V C C F D B               W T X C Z F C
  F R Q O I P X           T G B Y I H Z
  Y I F C A H Q Z E Z R F V Z H D I C M
    M C J V E Z H M F X N L G T R J W
      J O L T T G T V R J A T F A G
        K M H Y X D O C D L A J C
          E C O P D X M J F J R
              O I R J O J A

Smiley Face

>>> p = Puzzle(21)
>>> p.apply_mask(Rectangle(2,6, position=(6,4), method=3))
>>> p.apply_mask(Rectangle(2,6, position=(13,4), method=3))
>>> p.apply_mask(Rectangle(9,2, position=(6,14), method=3))
>>> p.apply_mask(Rectangle(2,2, position=(5,13), method=3))
>>> p.apply_mask(Rectangle(2,2, position=(14,13), method=3))

              P T X L S G P
          J N Y H M I I N A Z T
        Y F W D U S N I D V S J J
      C P X T K Z Z R L L H S H D E
    Y B M X     B S V J I     A S Q P
  N Z R Z M     A L P R O     I U S P H
  L U L N L     K H E Y H     E O D Q D
S Q D I V O     B P E E C     O H D Y C J
B R B L I Y     K K P K J     T C I E L N
Y S Z T Q S     Y Q P M G     C M Q R N U
D J X A R V U F O R J H K D E N R E F N T
R Q C K N F F O L I S W A X K L U S U G D
A T B B J C O W P K U Q Q E U H Z Y T Y K
H O W J R     C D F S P O O     W K S H S
  O Q M C                       B L O L
  G A M A B                   F S N G K
    Q B A D X E I Q M D H B P A L E V
      J L B E Q I Q T Y H V V Q O Y
        X U G E T H A S P P G C T
          Q G N L V Y S S S Q J
              V R R R B E D

Tree

>>> p = Puzzle(15)
>>> p.apply_mask(EquilateralTriangle())
>>> p.apply_mask(Rectangle(3,3, position=(6,12), method=2))

              V
            D E Y
            M G E
          U G K W R
          F N B W D
        Z J Q Y K P R
        P E P A W X S
      Z L K Q N P N T W
      U D B M Z H R N I
    I H Z V T X I H F I W
    A E D X N B E U S V U
  S Y K D R A T C V M L G K
            F I Q
            A A S
            U Z M

Six Pointed Star

>>> p = Puzzle(21)
>>> p.apply_mask(EquilateralTriangle())
>>> p.apply_mask(EquilateralTriangle(rotation=180))

                    D
                  S H L
                  E O Y
                E L L L H
                A Z X C F
  L Z W O J Y P F Y L Q C D Y R F O C E
    O S F T K K R B P C X O O D O Y O
    A B T K K L T X B J K J E Q T O Q
      I G F P S O E O J X N C P W T
      B Q P W G T S G L H N O O U K
        D E E E W B H O Z J U D R
      B X E M O K U F X A V Z T X D
      H I D R C R B R S M R I F P R
    G O H W N G Y P A J M D F F K K B
    L B F T V R D T V V Q D G B U N C
  R T M N X B H I I M I Q G X Z C Y D A
                H S U L N
                G Y E Y S
                  R Q P
                  G S E
                    N

Cross

>>> p = Puzzle(15)
>>> p.apply_mask(Rectangle(3,15, position=(6,0)))
>>> p.apply_mask(Rectangle(15,3, position=(0,6), method=2))
>>> p.invert_masking()

M B K U J J       E E P O Z I
B K G V S K       Q F S J H Z
Y G X S F P       T E C V L Y
K T S R P B       K C D N D Z
F U K Q N W       P R K H C D
Q W L F W O       R T A H J E



X K S C G E       O U N J K K
R P F O U Y       J L J L K H
L A P O A R       K G U P C V
T J C Y K W       U M W A J Y
I P M V T T       I U Y G S K
Y J J R T V       B T M L O D

Inverted Cross In Circle

p = Puzzle(21)
p.apply_mask(Circle())
p.apply_mask(Rectangle(31,3, position=(0,9), method=3))
p.apply_mask(Rectangle(3,31, position=(9,0), method=3))

              H N       S B
          G K I F       U C F V
        K J F Z S       R S H X U
      O X C N D U       L R Z O H K
    I P P Q I L I       Z H N A B I A
  C M O P B P T E       W A I P Y P I E
  A P B S V D S J       X N U E G O A M
P I O J D K T Z A       D T K U A B I T I
S K S T G O Z M W       I L E D T N J C T



L L N B I I Z W J       M N Z H P K A O D
H I E Z E Y W A Q       L N M Y L Y M O X
  T G R W M C K D       M E T I D P Q N
  J M K K X U C Z       X E G A X B B O
    N U W C X M G       U C L G F S Y
      F M K C L S       I M M V D L
        A R G I B       R A J I Q
          L C X P       S M P I
              F U       Q K

"Hole-y" Cross 😉

>>> p = Puzzle(21)
>>> p.apply_mask(Rectangle(7,7, method=3))
>>> p.apply_mask(Rectangle(7,7, position=(14,0), method=3))
>>> p.apply_mask(Rectangle(7,7, position=(0,14), method=3))
>>> p.apply_mask(Rectangle(7,7, position=(14,14), method=3))
>>> p.apply_mask(Ellipse(5,5, method=3))

              K D G N K C N
              S S H U S S K
              S C H L N I N
              M G A J U G E
              C U Y U M H B
              M R V I Z I R
              I O Q Y H P J
K D G P X H X O K Q U R F K W K J U I A X
M F Y N J Y U T M       M C Q T F E T Q T
C C H O E J U S           I G L J U E Y E
E R I E T G S K           K J C D Y H C Y
Z F D J N F I Y           M X I C T Q R L
B Y H N D H P A E       O X C C N O G L M
W A H A O H F Z D L Y Z U W E Q T G C Y G
              X K A R X L P
              H T T F I A F
              I P M K V G A
              G D Z S O Y G
              T J X F P W O
              Y S V E R X T
              R F E Q U S B

Checkboard

>>> p = Puzzle(30)
>>> p.invert_masking()
>>> for x in range(0, 30, 5):
>>>     for y in range(0, 30, 5):
>>>         if (x//5) % 2 == 0 and (y//2) % 5 == 0:
>>>             p.apply_mask(Rectangle(5,5, position=(x,y), method=2))
>>>         if (x//5) % 2 != 0 and (y//2) % 5 != 0:
>>>             p.apply_mask(Rectangle(5,5, position=(x,y), method=2))

O B W Z O           B R W U B           C J M N G
N J K D Y           U X U I J           K N W J I
E U R S H           H R A A F           J F L A F
Q T F U A           O B E O H           N Q L S O
I O S X N           I A H N U           K M U Z M
          N D J W K           W W W F S           G Q Q M Y
          D Q E F K           O I L Z F           J U T V S
          E B B M Y           O F Y B C           V T Y Z K
          B T I Y M           O N A L C           M T T A M
          F W P I U           R V E E R           P J I F E
A P W M Q           K M U J V           I C S A B
K G I S H           V P H O S           V J I X A
K K B M F           X X Y P J           D S O H P
R D I R B           V Z S E M           X W U A F
E D G T O           Q Z T L J           E G T Y W
          T X E H E           H M F T N           Q X A C T
          A T H C Q           J D B F V           E A U M X
          D S H S A           A M U N E           V J Q C C
          E V H W T           U O M T I           A Q X T D
          I L U Y M           S P P A C           R W X B Q
V T C F W           Z D Y N K           F P I K S
B C D I U           D M I P Y           P C G H T
O P F E U           Q K R J Q           M Q Y H Z
J Z D H Q           L L G W G           R N L V I
Z Q J S Z           M I G Z P           K X W D C
          Y L U H I           M R D J O           K G Y H U
          M L A U T           E D P X Y           N W Q V U
          V P O N X           T T H C S           W J B A R
          C U I O E           G P I H S           F J O F A
          W G Q E N           D C L A S           L L V M V

from word-search-generator.

duck57 avatar duck57 commented on June 16, 2024

Those look amazing! Haven't had too much time to do some proper code review.

from word-search-generator.

joshbduncan avatar joshbduncan commented on June 16, 2024

Implemented in v3

from word-search-generator.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.