Click here to Skip to main content
14,035,430 members
Click here to Skip to main content
Add your own
alternative version

Stats

2.9K views
100 downloads
1 bookmarked
Posted 14 Apr 2019
Licenced CPOL

Latin Crossword Puzzle - Ludum Verborum

, 14 Apr 2019
Rate this:
Please Sign up or sign in to vote.
a Latin crossword puzzle with sprite animations

Introduction

This Latin Crossword Puzzle is built off of and relies entirely on my Latin Project.  If you have any interest in the dead language I strongly recommend you have a look.  

This crossword puzzle application is a fun game to play with cool animations, multiple difficulty levels that will help any beginner Latin student and challenge the most skilled Latin scholars.  There are only five files listed to download above but there are several dozen at the end of this article.  These first five are the source code, three sprites for the game's animations and an XML file containing puzzles ready to be played, while the rest of the files are all part of the database that makes generating random crossword puzzles possible.

You cannot run both TheLatinProject & LatinCrosswordPuzzle simultaneously on the same computer because they share a common database.

In this article I will first explain how to download all the files and set it up in order to get things working.

Then a quick explanation on how to play the game.  This will be brief because the user-interface is very intuitive and should not be any trouble for anyone.  

Finally, I will discuss the code :

  1.  how the data-base was built 
  2.  how to generate crossword puzzles from the data
  3.  the user-interface
  4.  the graphics and the many Finite-State-Machines that govern the animated character behaviors

Background

I had made a crossword puzzle before using an English dictionary as a source for both the words and their clues but never published my work at that time, and now that game is lost.  This project is very similar with regards to generating the puzzles but has the added element of an animated Magister to help the player along.   If magister happens to have a dancing monkey and a bow-and-arrow, all the better.

Extracting the Files

There are some 30+ files to download and extract and I won't apologise.  If you have the LatinProject working its because you know what  you're doing and its not that scary.  It may be a hassle but well worth it.  You will find no other Latin Crossword Puzzle anywhere and this one's pretty cool.

So here it is.  All the files are named so that it's easy to extract them where they need to be extracted.  The first thing you're going to have to do is create 3 new sub directories in your existing (and working) Latin Project(recent C#2017 version) directories.  Have a look at the screen capture below that shows you what your c:\Latin\Data\ directory should look like when you've created these new sub-directories.

Now that you've created these sub-directories you can start to download and extract all the files listed at the end of this article.  The file nomenclature is the same as TheLatinProject.  The text after the first under-score briefly describes the content of the compressed file while the text before the first under-score shows you where that file needs to be extracted. 

e.g.    the file named  "c.Latin.Data.CrosswordPuzzle.Games_CW_PuzzleGenerator_files_.zip

contains the CW_PuzzleGenerator_files and must be extracted at "c:\Latin\Data\CrosswordPuzzle\Games\".

Follow this rule and you'll be done in no time.

If you don't download all the files and you have a running version of TheLatinProject you'll have to go into the source code's main form and find the Init() function, here shown below, and change the

if (true)

 line to

if (false)

<font color="#0b0813">
</font><font color="#0b0813">void Init()
{
    if (true)
    { // normal play
        if (classCW_XML.intNumPuzzlesWritten < classCW_PuzzleGenerator.conMaxPuzzlesGenerated)
            bckCW_PuzzleGenerator.RunWorkerAsync();
    }
    else
    { // rebuild database
        bckCW_DataBuilder.RunWorkerAsync();
    }
    placeObjects();
    drawPuzzle();
}</font><font color="#0b0813">
</font>

  Just be sure not to start a new game or do anything beyong disabling Magister by using the right-mouse context menu and selecting the Magister option.  It takes +36 hours of uninterrupted processing to complete!  So you're probaby better off downloading and extracting these files instead.

Let me introduce to you your new Latin teacher.  He's very wise.  He juggles, he dances and he'll help you with your latin.  We'll have a better look at him later.

Once you have the source code ready to run you must be certain that the sprite files (magister.sp3, hawk.sp3 & monkey.sp3) are downloaded and extracted in the c:\Latin\Data\CrosswordPuzzle directory.

At this point you should be set with

  1.  the 3 sprite (.sp3) files in the                        in the c:\Latin\Data\CrosswordPuzzle\
  2.  Latin_CW_0000.bin                                     in the c:\Latin\Data\CrosswordPuzzle\Data
  3.  Latin_CW_0000.LL to Latin_CW_0140.LL   in the c:\Latin\Data\CrosswordPuzzle\Data
  4.  CrossWordPuzzles.xml                               in the c:\Latin\Data\CrosswordPuzzle\Games

and now you're ready.

Playing the Game

Crossword puzzles have been around since the late 18th century and a quick look at Wikipedia will tell you that there are variations on themes and design patterns that seem to be cultural and language dependent.  This Latin crossword puzzle is, of course, dependent on the Latin language but has no particular theme or design.  They're randomly generated with words found in the Latin dictionary and the clues you are given depend on the level of difficulty you're currently playing.  All the puzzles, regardless of difficulty level, are generated in the same way, but the clues for the easier levels give you more information to work with.

The concept of crossword puzzles is simple : fill in the blanks with the correct letters to solve the puzzle.

Context Menu

You can use the context menu by right-clicking the screen with your mouse and selecting one of the various options:

  1.  New - start a new game
  2.  Load - load a saved game from file
  3.  Save - save the current game to file
  4.  Magister - toggle the magister on/off
  5.  Difficulty - select from either Easy - Normal - or Difficulty 
  6.  Font - select the font used to display the clues onto the screen
  7.  Quit - to exit the app

Puzzle Screen 

The screen is divided into two separate parts. To the left you have the puzzle with all its blank spaces where you are expected to write the appropriate letters to fill in the answers.  And to the right you will see the area where the clues to each word will be provided.  To select the word you want to fill in, just left click any character in that word and it will be highlighted to tell you that that is the word you're working on.  If the puzzle square you've selected is part of both a horizontal word and a vertical word the horizontal word will be highlighted first, click the same square again and the selected word will alternate between that square's horizontal and vertical words.  The flashing square is your cursor showing your which square you are about to fill in using the keyboard.  The screen capture below shows the (10, 6) square is 'blinked' black in the selected Yellow word that starts at V(10,0) as described in the clue on the right of the screen 'cuneavisse'.

Both screen captures below demonstrate clues that are provided for the Normal difficulty level.

You can use the arrow keys to move around.  When you combine the arrow keys with the control key you can jump to the next 'cross-road' on your puzzling way.  The End & Home keys will make the cursor jump to either end of the current word.  You can also delete a word square's text by pressing the delete key.

If you've selected the easy skill-level the game will behave slightly differently - at the easy level, the letters you enter into the puzzle will appear in green when they are correct, in red when they are wrong.  Also, the game interface will not permit you to over-write a puzzle square that is correct in the easy level.

Clues 

When you've selected a word you'll see that word's clue appear on the right side of the screen.  The numbers at the top left of this information is the word's 'address' in the puzzle in (X, Y) coordinates but you don't really need to know that because the word will appear highlighted in yellow once you've clicked on it.  The clues you are provided will differ depending on the skill level you've selected :

Easy - the easiest level of play will provide  you with the most information to help you figure out what word you're looking for.  At this level the clue will include the word's heading (as it appears in the dictionary) along with its full definition.  Using this word-heading information you will have to either decline or conjugate the word in the specific form which the word's clue provides.

Normal - this skill-level is somewhat more difficult than the easy level.  Not only are the letters on the screen no longer green/red telling you whether they're right or wrong but the clue may consist of the word's heading and a quote from an ancient Latin text with that word excised from the text.  You won't be told what part of speech it is, which conjugation or declension, so you'll have to read the text and try your best to figure out what case, number and gender is required when you're not dealing with a verb tense and person.  All the words on the puzzle should cross with other words, so if you're having trouble with one word, move on to the next and come back to it later, it may become easier once you've filled in some of the other words that cross it.

Difficult - this is where things get complicated.  Here you may be given the word's definition as it appears in the dictionary, except you won't be provided with that word's heading or the case, number, gender or person that word is to be declined/conjugated.  Sometimes, you'll be provided with a quote that has a red-line where the word you're looking for has been excised from it and you'll just have to work it out from memory or understanding of the text to fill in the blank.

The image below is an example of a Quote-style clue provided for a Normal level of play.  You can see that the player is given the word's heading but not the case, number, tense or person.

You can select the skill-level of play by using the context menu and clicking on the Difficulty option listed there.

If you're having difficulty with some of the latin that's on the screen, you can left-click on any of the text in the clue and be provided with a definition of the word you selected if one is available.  You won't be able to tell the app to give you all that word's various spellings like you would with TheLatinProject but the definitions of some of the words in an ancient latin quote could help you figure out the spelling of the word you're looking for.  It might help your Latin, too.

Magister 

Magister is very enthusiastic about Latin and he can be a lot of help.  No matter which difficulty level you're playing at, he will be ready to lend you a hand to solve your puzzle, he'll just be a little quicker about it if you're just a beginner and need encouragement.  You can toggle him on/off by right-clicking the mouse and summoning the context menu then selecting the Magister option there.

You'll notice by the way magister is behaving whether or not you're doing well in solving your puzzle. If you have any errors on the screen, Magister will become tense and may start to pace back and forth until you either get rid of the error or he get's so frustrated at seeing a mistake that he takes it upon himself to walk all over your puzzle and yank the incorrect letter off the screen, throw it or kick it or feed it to the birds.  He's only trying to help so don't get mad at him, he's just really passionate about Latin.

You may also notice where his attention is while you're playing.  Unless he's distracted by his best-friend Simius, he'll either be following your mouse-cursor around or, if you've made a mistake in the puzzle, he may focus his nervous attention entirely on the mistake you've made, anxious to go over there and correct it.  So, you can tell whether or not you've just made a mistake depending on whether or not he's looking at your mouse cursor.  That is, unless he's too busy juggling to look at the game board...

You'll notice a timer at the bottom right of the screen.  Don't worry its not a dooms-day clock, there are no bombs here and no one is keeping score.  No, what the timer tells you is how long you'll have to wait until you can ask Magister to fill in a blank, instead of removing an error.  When the timer runs out a 'Help' button will appear and you can then select the word you're having trouble with and press the 'Help' button.  The timer will restart and the next thing you know, Magister will stop whatever he's doing and go fetch a pen, then come back and fill in one of the empty blanks in the word you've selected.  Don't be embarassed if you ever need help, Latin crosswords can be difficult and he's only too glad to do it.

The Code

And now for a look inside :

 how the data-base was built

The puzzle generator builds off a data-base specifically designed to facilitate the building of a crossword puzzle.  If you have a general idea of how TheLatinProject's Look-Up Table works then you'll know that it consists of a binary-tree of the 772 000 different spellings of the 24 000 words in the Cassell's Dictionary.  The Crossword Puzzle's data is derived from those 772 000 different word spellings.  The code that builds this data-base first goes through each of these records in the sequence they were entered into the LUT's binary-tree file (starting from record number zero and proceeding incrementally through to the last 772 000th one) but not each spelling produces a viable word for the LatinCrosswordPuzzle game since many of them have more than one source file which would produce misleading clues when those clues are in the form of an ancient latin quote.  For example, any noun derived from an adjective or adjective derived from a noun, such as clausum, -i neuter noun is derived from the perfect passive participle of the verb claudo, claudere, clausi, clausum.  Since the word clausum could be either the adjective clausus, -a, -um,  the noun or the perfect passive participle the app could not differentiate between the three and would randomly pick one when displaying the clue along with a quote.  There are not very many examples where this difference is quite subtle but in those cases where it is not, I found it is completely unacceptable.  So to avoid that problem all word-spellings that have more than one source word are considered invalid and are not included into the crossword puzzle generator data-base.  With 772 000 candidates to choose from, I figured this was a reasonable solution.  Judging by the 134 Linked-List files that were generated in this final version compared to the 141 LL files that were generated before, that means that there are about 734 000 different word(spellings) in the current data-base, or %95 of the original.

The source-code on how it does this is fairly straight-forward and shown below:

bool bolValidWord = true;
try
{
    cLUT_BinRec = classLatin_LUT.LUT_BinRec_Load(intCW_Build_Index++);

    // test for and reject any word spelling that has more than one source filename
    List<string> lstFilenames = new List<string>();
    classLatin_LUT_LL_Record cLatin_LL = classLatin_LUT.LUT_LL_Record_Load(cLUT_BinRec.LL);
    while (cLatin_LL != null && bolValidWord)
    {
        if (!lstFilenames.Contains(cLatin_LL.filename))
            lstFilenames.Add(cLatin_LL.filename);

        if (lstFilenames.Count<2 && cLatin_LL.next >= 0)
            cLatin_LL = classLatin_LUT.LUT_LL_Record_Load(cLatin_LL.next);
        else
            cLatin_LL = null;
        bolValidWord = lstFilenames.Count <= 1;
    }

}
catch (Exception)
{
    goto endPosition;
}
</string></string>

Once a word is considered valid and can be inserted into the data-base, it is then scanned from left to right using two nested for-loops to sort the words according to the letters and their positions relative to each other.  Each possible two-letter combination of each word is used as a sorting device to generate a unique code for those two combinations, e.g. first-letter 'C' at position 2, second-letter 'B' in position 4 of the word necabo will produce a unique code that combines C2B4 into 4 characters regardless whether the positions in arabic decimal numerals requires two characters rather than one(like the 12th position or any number higher than 9).   This four-letter code is then used as the search-key in a binary-tree, the leaves of which lead to a circular-linked-list.  And the data fields of each of these linked-list items contains the word to be included into the tree.

Its not quite that simple.  Actually, the circular linked-lists are sorted by word-length into a second binary tree(word-length) that branches off from the leaf of the first tree(4-letter code defining two letter combination) then each leaf of these secondary Bin-trees point to a circular-linked-list that holds words of identical length with matching 2-letter combination(idential 4-letter code).

The reason why I used circular linked lists is so that the app could 'spin-the-wheel' of random chance and not use the same words for every puzzle.  Using a singly-linked list would require it to be read from beginning to end before a truly random selection could be made which is much too slow to generate puzzles in real-time.  The alternate design choice of cycling through all the options seems more reasonable since the original LUT bin-tree was generated using a random order, they are all entered randomly and appear in that random order in the LatinProject LUT file sourced to get the 772 000 word entries.  Also, the puzzle generator keeps track of the words that were used in the most recent 25 games generated to be certain not to re-use the same word twice within 25 games, more on this later.

The source-code that generates the 4-letter codes for the main binary-tree is shown below :

public static string getKey(char chr1, int int1, char chr2, int int2)
{
    return ((chr1.ToString()
             + chr2.ToString()
             + Convert.ToChar('A' + int1)).ToString()
             + (Convert.ToChar('A' + int2))).ToString();
}

you can find it in the 

public class classCW_BinTree_Record

The first crossword puzzle I wrote some years ago used a similar system but converted the 4-letter code into an integer value using a complicated multi-value based counting system where the first and third 'decimal' place where of base 26 and the other two were base-40(max word size) and their sum resulted in a unique integer which was then used as an index in a random-access file.  This system was considerably faster because it bypassed the binary-tree which this most recent version currently uses but required a much larger file in order to accomodate all the two-letter combinations that are not valid 2-letter.

Since the crossword puzzle does not attempt to generate puzzles with words that run immediately adjacent to each other along the same line, no two adjacent letter combinations are entered into the database.  See below :

if (validEntry(strWord))
{
    for (int int_1 = 0; int_1 < strWord.Length - 2; int_1++)
    {
        for (int int_2 = int_1 + 2; int_2 < strWord.Length; int_2++)
        {
            classCW_BinaryTree.classCW_BinTree_Record cBinRec 
                     = new classCW_BinaryTree.classCW_BinTree_Record(strWord, int_1, int_2);

            classLatin_LUT_LL_Record cLUT_LL 
                     = classLatin_LUT.LUT_LL_Record_Load(cLUT_BinRec.LL);

            classCW_BinaryTree.classCW_LL_Record cLL 
                     = new classCW_BinaryTree.classCW_LL_Record(strWord, cLUT_LL.filename);
            classCW_BinaryTree.CW_BinTree_Insert(ref cBinRec, ref cLL);
        }
    }
}

here's a diagram of what the database looks like :

although the code overseeing the rebuilding of the Crossword Puzzle data-base is in the 

<font color="#0b0813">class classCW_BuildDataTree</font>

most of the work involved in inserting and retrieving data from the data base is in 

<font color="#0b0813">classCW_BinaryTree.</font>

How to generate crossword puzzles from the data

To start generating puzzles using this data, the app first randomly selects two two-letter combinations and searches the data-base for valid entries.  Without concerning itself with the generating of clues for the moment, it keeps a running list of

  1.  all the letters currently on the screen (position & value)
  2.  list of horizontal words (spelling and start location)
  3.  list of vertical words(spelling and start location)

 

Once it has added these two words and positioned them at the top/left & bottom/right of the puzzle it begins a simple algorithm :

- randomly selects a puzzle square and removes it from the running list (1 above)

- tests if a word can be added horizontally

try to enter a word horizontally if it can

   if it fails to enter a word horizontally

it tries to enter one vertically

- when entering a word:

- using the letter/position it has selected at random (that is already on the board)

- it measures the distance it has before and after that letter

- decides whether to use the randomly selected letter as the 1st or 2nd in 2-letter combination

- either uses another existing letter on the board as the other letter in 2-letter combination 

or picks a second letter and its position at random if it cannot find or use one already present

- measures the maximum length of the word that can fit in the puzzle's current configuration

- plugs these parameters into the data-base search engine in the form of a 4-letter search key and maximum word size

- since each element in the circular-linked-lists contains the filename of the word that begat that particular spelling, and the Puzzle generator keeps track of the filenames of all the words it has used in the previous 25 puzzles it has generated, those words that are derived from the same dictionary source file are rejected.

- when a new word has been selected which fits into the existing puzzle

- it is added to the appropriate word list(horizontal/vertical)

- each letter position in this new word is added to the existing list of puzzle squares 

- eventually it can no longer easily find words to add to the puzzle and runs out of puzzles-squares for it to try, then quits and saves the puzzle in the CrossWordPuzzles.xml file.

While developing this app I spent over a month and a half(two?) testing (playing) and finding more and more errors in the LatinProject's LUT that resulted in unsatisfactory Latin puzzle words.  Sometimes, the Latin was so off that I had to resort to saving the game and loading the XML file to see what the puzzle word was... all too often abysmally spelled.  After making at least a dozen LUT rebuilds and then rebuilding the PuzzleGenerator database, it now looks like its promising to give fewer embarassing errors.  So, now that the development phase of this project is done, I've added an encryption function to the puzzle-file-storage and it is no longer possible to simple save the game and read the answer from the XML files.  If you want to implement a cheat mode, you're welcome to tinker with the classCW_XML file.

All the puzzle generating is done in the <span style="display: none;"> </span>classCW_PuzzleGenerator.  

public const int conMaxPuzzlesGenerated = 100;
const int conCW_WordsUsedRecently_MaxPuzzlesInList = 25;

The two constants shown above are found in the classCW_PuzzleGenerator and control the maximum number of puzzles maintained in the CrossWordPuzzles.xml file as well as the number of puzzles from which any given word-source may not be allowed to be reused.  Setting the conMaxPuzzlesGenerated value too high only serves to slow down the launching of a New game.  And restricting the use of the same word in too many consecutive games will give the Puzzle-Generator trouble and may result in puzzles with only a few words in them and too many blank spaces on the board.

When I first started this project and began generating puzzles I had not yet optimized the data-base to make it more convenient for the Puzzle-Generator to do its work and consequently the generating of puzzles was slow and laborious.  What is more, even though the puzzle-generator worked in the background while the player was busy solving puzzles, it could take ten-minutes to generate a puzzle and then it may be forced to quit before completing its work when the user exit the app.  To prevent the loss of this hard-earned partial-puzzle data, I implemented a partial-work save option which the puzzle-generator used to save the partially-generated puzzle and then begin from where it left off the next time the app was launched.  So now that a new puzzle can be generated in only seconds, a partial-work saving feature seems over-kill but it is still in use and working fine.

As soon as the App is launched the Puzzle-Generator background worker gets started generating new puzzles until the Maximum number of puzzles is reached, or it is interrupted and saves its work when the user starts a new puzzle and other background workers are needed to generate the clues during runtime.

 

Generating Clues

Because the user may both be slow to make a request for a new clue and still expect the computer to quickly provide a clue once one is requested, there are two back-ground workers that share the duty of generating the puzzle clues.  These two background workers work with the the same tools and alternate depending on the user's needs.  Their names are kind of long and difficult to pronounce,

public BackgroundWorker bckCW_ClueWriter_OnDemand = new BackgroundWorker();
public BackgroundWorker bckCW_ClueWriter_All = new BackgroundWorker();

so, we'll just call them OnDemand & All.  All gets to work as soon as a new puzzle is displayed on the screen.  It cycles through all the horizontal words listed in the puzzle first, then starts on the vertical words when it has completed those.  Whenever the user selects a new word and expects to see that word's clue appear on the screen, that word is tested to see if a clue has already been generated, if it has not already been generated then the dour All quits what he's doing and let's OnDemand obsequieously get to work diligently giving the user what the user wants when the user wants it.  When OnDemand is done, All gets back to work generating the rest of the clues.

After all the clues have been generated then, if necessary, the background Puzzle-generator kicks in and starts generating more puzzles.

Each word requires one clue for every skill level : easy, normal & difficult.  As I write this I'm considering changing how All goes about his day and the sequence in which he does his work, because at the time of writing All generates all three skill-level clues one after another before moving on to the next word.  This could be optimized ...  I hope I didn't make you wait too long while I made that change as I was writing this article.  Hate to make you wait like that.  So, as of this moment, All cycles through each word at the selected player skill-level, tests if each clue needs to be generated one word at a time through all the words, then cycles through each skill-level and repeats.  If the user changes skill-level then All will continue generating the wrong skill level until it gets around to generating the ones required, or is interrupted when the user selects a new word that has not had its clue generated and OnDemand provides the user with that clue before All starts again at the appropriate skill-level as if it had never done otherwise. 

The clue generating procedure was initially very slow, especially when it encountered a very common word and it was trying to find an appropriate quote from the library.  What was happening there, as I've mentioned in a recent update of TheLatinProject article mentioned at the top, the library ContentSearch was initially built and designed to provide the user with an exhaustive list of files that contain the word being searched by the user.  All fine and good for TheLatinProject, but for the CrossWord puzzle and clue-generating, loading a separate record for every file in the library that contains that word slowed things down unnecessarily.  Therefore, the entire TheLatinProject ContentSearch was rebuilt to accomodate this need.  It is similar to the Crossword Puzzle data-base described above, in that it has something similar to circular linked lists for the filenames of words being searched but they are not sorted by word length but rather by word-frequency, that is the number of times that word appears in each file.  More importantly, they are circular-linked-list-ish.  Instead of having a doubly-linked linked-list with the head and tail pointing to each other making them circular doubly-linked lists, the ContentSearch uses two pointers to a singly linked-list : head & tail.  The process of generating a quote involves finding the binary tree that points to the secondary tree which sorts the filenames by word length, then traverses that secondary binary tree.  With the list of pointers to the heads and tails to each linked list, it then randomly picks one, and takes a maximum of 10 filenames from those lists.  If the first lists are shorter than the maximum of 10, those lists are not altered but should that maximum number of 10 filenames end in the middle of any one linked-list(odds are it will) then the last list is altered by taking those first few elements in that list and moving them to the back of the list, altering the head & tail pointers in that linked-listed secondary binary tree leaf and all is right with the world of crossword puzzles.

Before making this change, all the filenames that contained a given word were in a single linked-list.  This list needed to be loaded completely for the clue generator to randomly select one of the thousands of files available, which was slowing things down considerably.  Once this change was made, the clue-generating process need only select some minimum number of files (one really) pick one at random and generate the quote.  Quick and easy.  trust me, its much better.

Putting the appropriate information on the screen is fairly simple and I won't get into it, but if you're interested the classGraphicText that actually draws the text onto a bitmap is explained in my GCIDE article that I wrote some ten years ago.

 

User-interface

there isn't much to this user-interface.  Now that's its time to write about it it occurs to me that there isn't very much to write about.  The only secret ingredient here is the

public TextBox txtUserInput = new TextBox();

this textbox is focused throughout the life of the app.  Whenever focus is rendered to the only other textbox, it is forced back onto txtUserInput.  There are only two functions that handle this textbox and those two are

private void TxtUserInput_KeyDown(object sender, KeyEventArgs e)
private void TxtUserInput_KeyPress(object sender, KeyPressEventArgs e)

You can have a look at these functions to see how all none-valid key-entries are rejected using the e.suppress textbox property.  This is also where the cursor is moved around using the Control/arrow key combinations.

Besides the keyboard interface, you have the mouse.  Since the screen is comprised entirely of a single picturebox which is refreshed as needed, the mouse interface consisted solely of MouseMove and MouseClick event handlers for that PictureBox.  There are only three regions of this picture box and those are : 

  1. the puzzle display area
  2. clue display area
  3. help button

Since the classGraphicText is used to display the clue's text, it is also used to determined what word is under the mouse in order to provide the user with Latin dictionary word entries when requested.  Mapping out the puzzle area is a simple matter of knowing the size of each puzzle square and dividing the mouse-position relative to the top-left region of the puzzle-area to resolve for the puzzle-square beneath the mouse-cursor's x-y coordinate.  There's only the help button that has a sub-region within the clue's region and is necessary only when the button is being drawn.

These event-handlers are easy to find in the main form and should not give you too much trouble.

 

Graphics and the many Finite-State-Machines

If you haven't already, I recommend you read my Sprite-Editor 2017 article.  It is the application that I wrote a few months ago and use to generate all my graphics.  Its pretty cool.  In this app, the Magister, Monkey and Hawk characters are all sprites.  Each one a different sprite file as you've seen in the list of files to download.  Again, if you want to learn more about how to make sprites and generate some quick and easy animations, I refer you to my previous article mentioned at the start of this paragraph.

The other objects that move around on the screen are either Missiles or Letters and both of these types have a class of their own that keeps track of their positions and velocities.  For example, whenever Magister is juggling.  Magister is only one sprite on the screen at that time and the letters he is juggling are separate entities called Letters.  The Magister sprite's Juggle animation only shows Magister moving his hands in the motion of juggling without anything in them.  The juggling is done in the app by measuring the position of the hand opposite of the one that is throwing at the time when that opposite hand will catch the letter he is about to throw.  Then it calculates a trajectory for the letter to travel upwards to some predetermined mid-point then back down again in time to reach the position the opposite hand will be when its time for him to catch it.  Left, right, left, right, three letters flying around two in the air at a time and round and round he goes.  You can see this in the classMagister's Juggle() function.  Shown below is some of the code that does this.  You can have a look at the source-code when you download it to see how it works but for the sake of brevity I cut out most of the different cases in this Switch-Case statement only leaving the LeftThrow to demonstrate how this was done.

enuJugglingFrames eJugglingFrame = (enuJugglingFrames)cData.cAnimationData.intFrameIndex;
switch (eJugglingFrame)
{
    case enuJugglingFrames.LeftCatch:
        {
         ...
        }
        break;

    case enuJugglingFrames.LeftThrow:
        {

            if (((classAnimationTag)cData
                                    .cAnimationData
                                    .cAnimation
                                    .tag).intRepeatAnimation > 1)
            {   // calculate starting position of thrown letter
                classAnimation_Frame cFrame_start 
                          = cData
                              .cAnimationData
                              .cAnimation.lstFrames[(int)enuJugglingFrames.LeftThrow];
                classAnimation_Frame_LimbData cLeftHandData 
                          = (classAnimation_Frame_LimbData)cFrame_start
                                    .getData(enuMagister_Limbs.Hand_Left.ToString());
                Point ptStart = cMath.AddTwoPoints(cData.pt, cLeftHandData.ptDrawCenter);

                // calculate end position of thrown letter 
                classAnimation_Frame cFrame_End 
                          = cData
                              .cAnimationData
                              .cAnimation
                              .lstFrames[(int)enuJugglingFrames.RightCatch];
                classAnimation_Frame_LimbData cRightHandData 
                          = (classAnimation_Frame_LimbData)cFrame_End
                              .getData(enuMagister_Limbs.Hand_Right.ToString());
                Point ptEnd = cMath.AddTwoPoints(cData.pt, cRightHandData.ptDrawCenter);

                // calculate Apex position of thrown letter
                classAnimation_Frame_LimbData cHatData 
                          = (classAnimation_Frame_LimbData)cFrame_start
                              .getData(enuMagister_Limbs.Hat.ToString());
                Point ptApex = cMath.AddTwoPoints(cData.pt, cHatData.ptDrawCenter);
                ptApex.X = cData.pt.X - (ptApex.X - ptEnd.X) / 2;
                ptApex.Y -= 100;

                classLetter cLetterLeft = lstJugglingLetters[0];
                lstJugglingLetters.Remove(cLetterLeft);
                lstJugglingLetters.Add(cLetterLeft);

                // calculate positions of ThrownLetter as it climbs
                double[] dblFraction_UpPath = { 0.4, 0.7, 0.9, 1.0 };
                PointF ptfDelta_Up = new PointF(ptApex.X - ptStart.X, ptApex.Y - ptStart.Y);
                cLetterLeft.lstPath.Clear();
                for (int intStepUpCounter = 0; 
                     intStepUpCounter < dblFraction_UpPath.Length; 
                     intStepUpCounter++)
                {
                    Point ptUp 
                       = new Point(ptStart.X 
                                   + (int)(ptfDelta_Up.X 
                                              * dblFraction_UpPath[intStepUpCounter]), 
                                   ptStart.Y 
                                   + (int)(ptfDelta_Up.Y 
                                              * dblFraction_UpPath[intStepUpCounter]));
                    cLetterLeft.lstPath.Add(ptUp);
                }

                // calculate positions of ThrownLetter as it falls
                double[] dblFraction_DownPath = { 0.1, 0.3, 0.6, 1.0 };
                PointF ptfDelta_Down = new PointF(ptEnd.X - ptApex.X, ptEnd.Y - ptApex.Y);

                for (int intStepDownCounter = 0; 
                     intStepDownCounter < dblFraction_DownPath.Length; 
                     intStepDownCounter++)
                {
                    Point ptDown 
                        = new Point(ptApex.X 
                                      + (int)(ptfDelta_Down.X 
                                                 * dblFraction_DownPath[intStepDownCounter]), 
                                    ptApex.Y 
                                      + (int)(ptfDelta_Down.Y 
                                                 * dblFraction_DownPath[intStepDownCounter]));
                    cLetterLeft.lstPath.Add(ptDown);
                }
                cLetterLeft.bolDraw 
                          = ((classAnimationTag)cData
                                                .cAnimationData
                                                .cAnimation
                                                .tag).intRepeatAnimation > 1;
            }
        }
        break;

    case enuJugglingFrames.RightCatch:
        {
         ...
        }
        break;

    case enuJugglingFrames.RightThrow:
        {
        ...
            }
        }
        break;

    case enuJugglingFrames.LeftWait:
    case enuJugglingFrames.RightWait:
        break;
}

This came off surprisingly easily, a lot easier than I expected.  The Hawk animation, however, was somewhat more difficult because even though the Monkey dances and tumbles and swings from a vine, he never climbs onto Magister's arm and he never touches the game squares in the puzzle area.  So the monkey may be cool to watch and encouraging to know that you can only see him at his antics when there are no errors on the board, the hawk's routine to fly across the screen, land on magister's arm, wait for Magister's instructions, then fly off and come down in steep dive to catch the wayward letter that doesn't belong where it is before flying off again with the misplaced letter gave me considerably more trouble.

Each sprite, Magister, Monkey & Hawk all have separate classes as well as separate Finite-State-Machines.  These FSMs are a relatively simple way to get characters to do what you want them to.  They are a list of 'states' that the character can be in and when in a given state that character knows just what to do because you've isolated it for that specific purpose.  In the case of Magister, he has these general states :

  1. Frolic,
  2. beginToWorry,
  3. Worry,
  4. makeACorrection,
  5. Celebrate
  6. & Help.

He'll frolic when there are no errors on the board.  To him that means: juggle, wave happily, dance or play the accordion.  When there's an error on the board, he'll 'begin-to-worry', here he doesn't quite pace nervously yet, but no longer juggles or plays the accordion, he'll just sort of wave distractedly while looking sideways towards the puzzle.  When a certain time-period elapses (cycles of the animations) and all the errors in the puzzle have not yet been corrected, he'll 'worry'.  Which, for Magister, means he'll pace back-and-forth nervously pausing only to wipe his brow and tap his foot with anxiety for the user's troubled latin.  At the end of each animation sequence there's a random chance he'll change state and 'MakeACorrection'.  When he's making a correction there is a separate FSM which guides him to select a form of correction (sledge-hammer, bow & arrow, swipe of the hand or hawk, to name a few) and which of the incorrectly placed letters off the board to correct.  When that sequence of finite states that comprise that particular FSM_MakeACorrection is completed he will then either fall into the 'Worry' state again, if there are still errors on the board, or he may frolic.

The Help state is only reached when the user presses the Help button.  And after he writes down a letter on the board to help the player, he may then celebrate (if the game is completed), Worry, if there are still errors on the board, or Frolic if there are not.

The only animation that was any different from most other animations is the accordion playing.  Have a look at it, if you've used the SpriteEditor or read that article you may find that animation a little odd because the hand turns a crank on the side of the accordion.  What I did was added the accordionBox, HandleShaft & Handle to the Magister Sprite.  Then I positioned the handle at the top of its rotation in the first frame of that animation.  The second frame showed the handle near the bottom.  I then generated the intermediate frames between those two using the SpriteEditor's AutoInsertIntermediateFrames feature and completed the handle's rotation.  Notice at no time has the Magister's hand reached for the handle, the handle at that point was rotating by itself.  So, I stepped through the animation's frames and positioned the hand over the handle for each frame and the job was done, Magister plays the grind-accordion for his pet-monkey (I'm aware that monkeys have a tail but have yet to bother changing the name of Simius's sprite in the source-code to reflect the fact that he's actually a chimpanzee and will continue to refer to him as a 'monkey' until Jane Goodall makes an official complaint.  love her, by the way).

And that's about all I want to write on the subject.

The various FSMs are included in the characters' respective classes.  I should point out that I was not consistent in the way I implemented these FSMs.  The first implementation of Magister's general FSM has two variables for the current-state and the next-state while most of the ones I implemented later only have the one current state and Switch-Case code refers to what happens at the end of the labeled case as opposed to the start.   Its not the best code.  but its stable and I'm tired of this project, so I'm not fixing what ain't broke.

 

Animation Tags

If you go to the classMagister you may notice that that is where all of the Magister's sprite's animation tags are defined.  The animation tags were not included in my Sprite Editor article and I only added them while working on the LatinCrossWordPuzzle project, so I'll mention them here.  Many Visual Studio objects such as TextBoxes and Panels and such have a Tag property that allows you to tie anything to that object by first converting it to a generalized type called object.  It turns out these extra bits of data can be very useful, so as easy as pie, I added one to the classSpriteAnimation.  Essentially, a Tag is nothing more than a pointer to an address that holds the generic type Object.  So here, the Magister's sprite has a classAnimationTag which is shown here below :

 

public class classAnimationTag
{
    public int intRepeatAnimation = 1;
    public int intActionFrame = 0;
    public Point[] ptSteps;
    public enum enuPathType { 
                independent, 
                X_only, 
                Y_only, 
                Y_tracks_X, 
                X_tracks_Y, 
                _numPathType};

    public classAnimationTag(ref classAnimation cAnimation, 
                                 int TimesRepeat, 
                                 int ActionFrame, 
                                 Point[] steps, 
                                 enuPathType pathType)
    {
        intRepeatAnimation = TimesRepeat;
        intActionFrame = ActionFrame;
        ptSteps = steps;
        PathType = pathType;
    }

    enuPathType _pathtype = enuPathType.Y_tracks_X;
    public enuPathType PathType
    {
        get { return _pathtype; }
        set { _pathtype = value; }
    }
}

When the Magister's sprite is initialized, all of its animations must have a tag.  These tags are comprised of the data shown above and then converted to object and assigned to their respective animations.  Some of the parameters for the classAnimationTag's instantiation may not be obvious so let's have a look:

  1. the first one is a reference to the animation it is tied to (redundant perhaps but convenient)

  2. TimesRepeat - tells the functions that handle Magister's animations how many times a specific animation is to be repeated before Magister moves on to something else.  Some animations are only to be executed once while others are set to random values when they are used (like dance or frolicking animations) while still others (like walk_left, walk_right) are set to 1 or 2 (perhaps 3) to measure out Magister's steps depending on how far he is walking in either direction.  More on this below when I talk about the classPath

  3. ActionFrame - some animations require an action to be performed mid-animation (e.g. the SledgeHammer animation must result in the Letter Magister is striking off the board to fly off the game board when the hammer reaches it).  These are handled at the beginning of the classMagister's nextFrame() function which uses a Switch/Case to determine what it is that is supposed to happen depending on which animation is running.

  4.  steps - the steps are an array of cartesian points that tell the animation handling functions where to place Magister(or other sprite) during any one frame of that animation.  Most animations do not result in Magister's position being changed and have a null list for steps, but Magister's walking animation, the Monkey's cartwheeling and the Hawk's flying all do.  There must be the exact number of points as there are frames in the animation and I will explain more below when I talk about classPath .

  5.  pathType - the classPath has several different ways of calculating how many pixels a given sprite will move for every frame of a specified animation and this is where that information is initialized.

classPath

The classPath was implemented to facilitate the moving of sprites from one point on the screen to another.  Since the CrosswordPuzzle does not perform any collision detection tests and the sprites will go to their destinations unimpeded, the classPath can easily calculate how many iterations of a given animation will be required to get the sprite there as well as how many pixels in the X, Y directions that sprite must be shifted to give it a smooth transition along its way.  How it works is not overly complicated.  As mentioned above, each animation has its own animation-tag which holds the data and parameters required to calculate the sprite's movements in the form of steps & pathType.  Since each point in the steps list details how far the sprite must be moved in the x-y axes, the sum of these points equals how far that sprite will move during one complete cycle of that animation.  Rarely will the sprite move exactly as far as a single cycle of the animation was originaly drawn to take him, therefore the classPath steps in the do the rest of the work for the specific start-location to end-location transition the character is intending to do while drawing a specified animation to get him there.

The total distance the sprite needs to travel is divided by the total distance it can travel under ideal circumstances through one cycle of the selected animation.  This number is then rounded up or down to the nearest whole number and that whole number is then used as the TimesRepeat field of the animation's tag.  This TimesRepeat field is then tested just as any regular integer variable throughout the completion of the sprite's transition from point A-to-B, keeping track of how many times the animation has iterated and then stopping, presumably, right where that sprite needs to be at the end of its movement.

Now that the classPath knows how many times the animation will be repeated, as mentioned above, rarely will this value be exactly what the animation's steps list of points adds up to.  Therefore, the fraction between the ideal distance to travel(that given by the sum of the steps) for a smooth animation and the distance it actually needs to travel to get to where it needs to be on the screen is used to modify the running sum of the steps array of points.

e.g. if 3 complete cycles of the animation moving the sprite the exact values stated in the steps list of points(which never change during the lifetime of the application) is greater then the total distance to travel then each step will be altered to reflect that difference so that the final sum is exactly what you need.

Instead of altering the individual Point values in the steps array, it takes a running sum of those points, divides this value by the total, then takes that fraction and multiplies it with the total distance it needs to travel to get more accurate results.  This is similar to taking a measuring tape and calculating where he needs to be at some frame n rather than using an 8' office-ruler and accumulating errors every frame.

Each of the three sprite characters (Magister, Hawk & Monkey) have their own class, and each of these classes has an instance of the classPath.  Whenever a character needs to move, its animation is identified and only then is the classPath's instance told the destination.  Since the classPath already knows which animation to use, it then does the calculations described above and creates a list of points where that character is going to be at each frame of each iteration of that animation.  Then, when the sprite needs to be drawn to the screen, the classPath provides the next point along the path and removes it from the list until it runs out of points just as the last iteration of that animation completes its full cycle and Magister is right there ready to kick an erroneous entry off the puzzle board, smile and saunter back to his spot near the right of the screen.

Problems Along the Way

There were a few problems during the development of this game.  When I first started this project I thought that the month and a half I had put into refreshing and bringing back to life TheLatinProject was pretty good.  Believing that all that work that I put into it had made it near perfect, I was sure that the Crossword Puzzle would be a quick cool way to improve something that was (is) already very good.  However, it is one thing to use TheLatinProject for a half-hour in the morning and not notice anything terribly wrong with it, quite another to play a latin crossword puzzle that sometimes conjugates verbs as well as Ozzy Osbourne and declines Latin nouns as well as the Taliban.  It became immediately obvious while playing the game that TheLatinProject had a few too many errors that could go by unnoticed for years when you use it casually but are glaringly wrong when they appear in the cross-word puzzle clue.  So time after time, I found and fixed Latin problems with TheLatinProject's Look-Up Table, recompiled the LUT (7 hours delay) then rebuilt the CrosswordPuzzle's data-base (innitially 10 days!!! now 36+ hours) only to have to repeat the process again after just a few hours of testing when more problems were found.  Needless to say TheLatinProject is much improved.  It is far better now than it was just a few months ago but after a dozen or so rebuilds and the frustrating delays these rebuilds entailed I am thoroughly fed-up with this project.  And will be glad to enjoy it now that its done.

(check for updates in the near future but for now its time for me to build an arcade style game or I will crack).

Enjoy this game, causa Latinam est Gaudium!

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Christ Kennedy
CEO unemployable
Canada Canada
Christ Kennedy grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University. He is currently living in a homeless shelter and eating way too many pastries awaiting the release of his latest novel "Paladin : An Origin Story".

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03 | 2.8.190424.1 | Last Updated 14 Apr 2019
Article Copyright 2019 by Christ Kennedy
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid