
Creating a HANGMAN game with Perl and SQLite to run in the Command Line
This project, on a high level, is basically a 3 step process, which I’ll segment out into smaller sub steps.
1. Get A list of words.
We could use an API, or some other source. I’m going to use an online word generator, and copy and paste the list as a .txt file in my text editor.
I should end up with a simple comma separated list that looks like this:
camel, hair, computer, apricot, nerf, gallop……..etc, etc, etc
2. Add words to an SQLite Table.
I’ll use two separate scripts. One to add all of my words to an SQL table, and a second script that plays the game, since I don’t need to run code that would add words to the database each time I play the game.
Instead of just a simple list of words, I’ll have a table with columns. These columns would include:
Word — TEXT (example: “abacus”)
Remaining Guesses — INTEGER ( example: 4)
Number of Correct Guesses — INTEGER ( example: 1)
Number of Incorrect Guesses — INTEGER ( example: 4)
Guessed Letters — TEXT (example: agxer)
Correctly Guessed Letters — TEXT (example: a)
Incorrectly Guessed Letters — TEXT (example: gxer)
Remaining Letters — TEXT ( example: _bcd_f_hijklmnopqrstuvw_yz )
Guessed Word — TEXT (example: _b_cus )
As you can see from this, my basic idea is that after each letter is guessed, I’ll update the table with the pertinent columns for that particular record.
3. Create a script that allows for user input, and updates table.
In this step, although there will be a lot of code, the process is actually pretty simple:
Retrieve a record from the database
Allow for user input ( guessing a letter )
Update the current record
STEP 1 — WORD LIST
First, I went to a site that produces random words similar to https://randomwordgenerator.com/. You could connect to an API, or any number of things to get this. In the end, I ended up with a text file of about 1,000 words, formatted like the following:
loot, import, boom, relief, stain, audience, heroin, conductor, frozen, reflection, constraint, mug, origin, wolf, fault, bury, ambiguous, weakness, soar, lion, mole, impress, bike, graduate…..
Because each of these variables will be single items, I won’t need to create additional tables for some of these columns. Technically, things like remaining letters, guessed letter, etc. could be arrays and stored as (a, b, c, d, e….). However, Instead, for each of these types of data, I’m storing it as a string and concatenating the results. For example, if the guessed letters thus far were (a, b, c, d), I would store it as a string “abcd”. Then if the next guessed letter is “e”, I would concatenate it to the current string using a syntax like: $string1 . $string2. The reason for doing this is to avoid a lot of code, and having to use loops.
Next, I’ll need to connect to an SQL Database:
my $dsn = “DBI:SQLite:hangman.sqlite”;
my %attr = (PrintError=>0, RaiseError=>1);
# connect to the database
my $dbh = DBI->connect($dsn, \%attr);
Finally, I’ll set up my table:
$dbh->do(‘PRAGMA foreign_keys = ON’);
$dbh->do(‘PRAGMA foreign_keys’);
my @ddl = (
‘CREATE TABLE GUESSEDWORDS (
id INTEGER UNIQUE NOT NULL,
WORD TEXT,
REMAINING_GUESSES INTEGER,
CORRECT INTEGER,
INCORRECT INTEGER,
GUESSED_LETTERS TEXT,
RIGHT_LETTERS TEXT,
WRONG_LETTERS TEXT,
REMAINING_LETTERS TEXT,
GUESSED_WORD TEXT
PRIMARY KEY(id)
)’,
);
for my $sql (@ddl) {
$dbh->do($sql);
}
Next, I’ll need to connect to my words. I have mine stored in a txt file which I’ll open with “open” and then loop through:
my $filename = ‘words.txt’;
# connect to and open the json file
my $json_text = do {
open(my $json_fh, “<:encoding(UTF-8)”, $filename)
or die(“Can’t open \$filename\”: $!\n”);
local $/;
<$json_fh>
};
Now, the entire word list is stored in a single string, in the variable $json_text. So naturally, I will want this in an array, so that I can loop over it and add data to each one. I’ll do this with the Perl split method.
my @words = split “, “, $json_text;
The previous data is saved in an array called @words. Most of the data for each word will be the same. For example, each word will start with 10 guesses, but the current guesses, as well as right or wrong guesses will all be 0. All of the guessed letters will be the same as well, and empty string. The only two pieces of data that will be different is the word itself, and the guessed letters, which will just be blanks, corresponding to the length of each word. So if the current word is “MOUSE”, the guessed letters field should be “_ _ _ _ _”. So I’ll loop over each word, and convert each word to a series of blanks, but insert both the word and the blanks into the current row.
foreach my $xr (@words) {
my $length = length $xr;
my @array = split //, $xr;
my @array2 = ();
foreach my $x (@array) {
push @array2, “_”
};
my $str = join ‘ ‘, @array2;
Now, my original word is stored in the variable $xr, and the blanks are stored in the variable $str. Still inside the foreach loop, I’ll create my query and insert statement:
my $query = “insert into CURRENTWORD (WORD, REMAINING_GUESSES, CORRECT, INCORRECT, GUESSED_LETTERS, RIGHT_LETTERS, WRONG_LETTERS, REMAINING_LETTERS, GUESSED_WORD, ACTIVE)
values (?,?,?,?,?,?,?,?,?,?) “;
my $statement = $dbh->prepare($query);
$statement->execute($xr, 10, 0, 0, “”, “”, “”, “abcdefghijklmnopqrstuvwxyz”, $str, “no”);
};
Now, we should have a table, with the above values stored for each word. We probably only need to run this file once, unless we want to update our word list.
STEP 2 — CREATING THE LOGIC
At this point, I already know the fields that I’m going to bring in from my database. So I’m going to declare these variables at the beginning of my Perl file, and reset the values later once I bring data in.
#!/usr/bin/perl
use DBI;
use strict;
use warnings;
my $wordID;
my $word1;
my $remaining_guesses1;
my $correct1;
my $incorrect1;
my $guessed_letters1;
my $right_letters1;
my $wrong_letters1;
my $remaining_letters1;
my $guessed_word1;
Then, I’ll create a welcome message that the user will see in the terminal when they run the file:
print “Welcome to Hangman! \n”;
Next, I’ll need to connect to the database, which will be the same code we used in the previous file to write to the database:
my $dsn = “DBI:SQLite:hangman.sqlite”;
my %attr = (PrintError=>0, RaiseError=>1);
# connect to the database
my $dbh = DBI->connect($dsn, \%attr);
Next, I’m going to query the database. I only want to return one record at a time, since the user will only need to guess a single word at once. I’ve thought about different ways to do this, like, “if the user has a partially guessed word, use that record.” “if not, then use any other record”. It seems like SQLite provides a way to do this through case statements. However, after spending a little while looking into that, it seemed more complicated than I originally thought. So, instead, I decided for a simpler alternative. I’m just going to sort the results each time, and pull the first result. If the user runs out of guesses or guesses the word, I’ll just delete that entry from the database, so the next time, it will actually move down to the next record. Here’s how I’ll make that query:
my $query2 = “SELECT * FROM CURRENTWORD
ORDER BY REMAINING_GUESSES ASC
LIMIT 1”;
my $statement2 = $dbh->prepare($query2);
$statement2->execute();
while (my @data = $statement2->fetchrow_array()) {
$wordID = $data[0];
$word1 = $data[1];
$remaining_guesses1 = $data[2];
$correct1 = $data[3];
$incorrect1 = $data[4];
$guessed_letters1 = $data[5];
$right_letters1 = $data[6];
$wrong_letters1 = $data[7];
$remaining_letters1 = $data[8];
$guessed_word1 = $data[9];
};
As you can see, I’m assorting the list in ascending order “ASC”, and using “LIMIT 1” to ensure only one record is returned. The data returned will be in an array, with the ID first as the primary Key of the row. Because we already set up the table, we know what order the remaining fields will be in, so next, I reset the values of all the variables to reflect the data that we’ve pulled from the table.
Now, I’m just going to use these variables to print some data to the user about the current word from the table:
print “You have this many remaining guesses for this word: $remaining_guesses1 \n”;
print “number of correct guesses so far: $correct1 \n”;
print “number of incorrect guesses so far: $incorrect1 \n”;
print “these are the letters you have guessed: $guessed_letters1 \n”;
print “these are the letters you have guessed correctly: $right_letters1 \n”;
print “these are the letters you have guessed incorrectly: $wrong_letters1 \n”;
print “these are the remaining letters: $remaining_letters1 \n”;
print “This is your current word: $guessed_word1 \n”;
Perfect. Now, I need to provide a way to allow for user input, which can be used with STDIN. STDIN and STDOUT are the standard way in Perl to use input and output. In this case, We’ll use the <STDIN>, which will put a cursor on the screen for the user, and after entering a character, and pressing enter, Perl will grab whatever was typed as input, and we can save it as a variable. Here’s how we’ll do that:
print “Please Pick a Letter: “;
my $letter = <STDIN>;
chomp $letter;
The user’s input is now saved as the variable $letter, which will use throughout the remaining logic to manipulate and add to the current data.
1. Did the user make a guess?:
$remaining_guesses1 = $remaining_guesses1–1;
Since at this point in the code, we know the user has submitted input, and our current remaining guesses is between 1 and 10, we’ll simply subtract that, save it as a variable and update the database with the new variable.
2. Was it a correct guess?:
sub getCorrect {
if (index($word1, $letter) != -1) {
return 1;
} else {
return 0;
}
};
my $getCorrect = $correct1 + getCorrect;
I’m defining a subroutine here to determine if the user made a correct guess. If there is an index of the letter in the current word, then it returns 1 (true), or 0 (false) if there is no index. And I’m storing that result as a variable.
3. Was it an incorrect guess?:
my $getCorrect = $correct1 + getCorrect;
sub getIncorrect {
if (index($word1, $letter) != -1) {
return 0;
} else {
return 1;
}
};
my $getIncorrect = $incorrect1 + getIncorrect;
This is the same as the above step, and probably unnecessary, but I’m including it anyway, because reasons.
4. What are the total guessed letters so far?:
my $alphabet = $remaining_letters1;
sub newGuessedLetters {
$alphabet =~ s/$letter/_/;
if ($guessed_letters1 eq “”){
return $letter;
} else {
return $guessed_letters1 . $letter;
}
}
my $newGuessed = newGuessedLetters;
Here, I’m using the variable $alphabet as the remaining letters. Then I’m using regex to replace the $letter that was guessed with the symbol “_”. Then I’m running the function and saving the return statement as a variable to update the database with. This variable will either contain one letter or multiple letters.
5. Add the current guessed letter to either right or wrong guessed letters:
sub getRightLetters {
if (getCorrect == 1) {
if ($right_letters1 eq “”){
$right_letters1 = $letter;
}
else {
$right_letters1 = $right_letters1 . $letter;
}
}elsif(getCorrect == 0) {
if ($wrong_letters1 eq “”){
$wrong_letters1 = $letter;
} else {
$wrong_letters1 = $wrong_letters1 . $letter;
}
}
};
getRightLetters;
Here, I’m simply seeing the result of the getCorrect function and with that information, updating either the $right_letters1 variable or the $wrong_letters1 variable, depending on the outcome.
6. What is the current guessed word?
sub returnBlanks {
my $string = $word1;
my $blanks = $guessed_word1;
my @blanks = split ‘ ‘, $blanks;
my @string = split ‘’, $string;
my $length = @string;
for (my $i=0; $i < $length; $i++) {
if ($string[$i] eq $letter){
splice @blanks, $i, 1, $letter;
}
}
my $scal = join(“ “, @blanks);
return $scal;
}
my $returnedString = returnBlanks;
This is probably the trickiest part, but basically we’re converting the current word and the current guessed word to a string. Then we’re looping through the word, and comparing each character against the guessed letter. If there’s a match, we splice the current letter into the array of blanks, and join the result to create a new string.
7. Now, let’s update the database with the new data we’ve collected:
$dbh->do(“UPDATE CURRENTWORD SET REMAINING_GUESSES = $remaining_guesses1 WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET CORRECT = $getCorrect WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET INCORRECT = $getIncorrect WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET GUESSED_LETTERS = ‘$newGuessed’ WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET RIGHT_LETTERS = ‘$right_letters1’ WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET WRONG_LETTERS = ‘$wrong_letters1’ WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET REMAINING_LETTERS = ‘$alphabet’ WHERE id = $wordID”);
$dbh->do(“UPDATE CURRENTWORD SET GUESSED_WORD = ‘$returnedString’ WHERE id = $wordID”);
sub deleteifComplete {
if (index($returnedString, “_”) == -1 || $remaining_guesses1 <= 0){
$dbh->do(“DELETE FROM CURRENTWORD WHERE id = $wordID”);
}
}
deleteifComplete;
The first function here simply updates the record with the new information in an UPDATE query. The second part, a function, decides if either the word has been guessed or the user has run out of guesses, and in either case, runs an DELETE query, deleting the current row. If this happens, the next time the user runs the script, they will get a new word.
I’m sure this may seem a little thrown together, but if you have any feedback, let me know. Thanks!
✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.