Comments (20)
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.
@duck57, I like masks. I feel like more people would understand that over filer.
from word-search-generator.
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.
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.
from word-search-generator.
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
orlist[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 likerandom.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
- Do I make these (or the selection functions) functions or methods of a Puzzle instance?
- 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.
@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.
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.
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.
For the sake of consistent nomenclature, should this feature be called "masks" or "filters"?
from word-search-generator.
- 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.
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.
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.
And I don't think it would be too hard to implement a bitmap mask using PIL...
from word-search-generator.
@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.
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):
- Merges in #17 to use it as the starting base [I started this on Tuesday, so it's before you've made your comments]
- 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. - Leaves
puzzle.py
mostly alone. The re-implementations are inmasks.py
- 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)
- The new mask implementation has been hooked up with WordSearch objects. They do correctly build the word searches around the fenced-off coordinates.
- 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 likegrow_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.
@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
- 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.
- 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.
- 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)
# # # # # # # # # # #
# * * # # # # # # # #
# * * * * # # # # # #
# * * * * * * # # # #
# * * * * * * * # # #
# # * * * * * # # # #
# # * * * * # # # # #
# # * * * # # # # # #
# # * * # # # # # # #
# # * # # # # # # # #
# # # # # # # # # # #
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.
>>> 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
# # # # # * * # # # # # #
# # # * * * * * * # # # #
# # * * * * * * * * * # #
# # * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * * *
* * * * * * * * * * * * *
* * * * * * * * * * * * #
# * * * * * * * * * * * #
# * * * * * * * * * * # #
# # * * * * * * * * * # #
# # # # * * * * * * # # #
# # # # # # * * # # # # #
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.
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.
Those look amazing! Haven't had too much time to do some proper code review.
from word-search-generator.
Implemented in v3
from word-search-generator.
Related Issues (20)
- Solution without random fill characters HOT 3
- find_a_fit does not care about secret directions HOT 1
- add hide_fillers to save? HOT 5
- Outputting shortened words of a solution HOT 7
- change default path word search.exe HOT 2
- Unbake assumptions about secret words [priority, valid direction] from the Game class HOT 11
- fpdf2 required version discrepancy in dev requirements HOT 4
- Draw a rectangle around the words HOT 4
- --no-validators results in error: unrecognized arguments: --no-validators HOT 6
- Quick Questions - Solution, Diff Level and word lists. HOT 10
- Add an option to generate puzzles with lowercase letters? HOT 6
- Multilingual Support HOT 7
- Upgrade of packages required HOT 5
- Despite space, its not placing words? HOT 14
- Themed word sets
- latest cli version word-search HOT 2
- Why is there no difficulty 6 level? HOT 7
- How can i remove ans key HOT 1
- CLI doesn't work for me on MacOS 14.2 HOT 2
- Custom Filler Alphabet/Characters HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from word-search-generator.