WORDLE in AGS?

Started by tilapisha, Sun 03/04/2022 18:18:41

Previous topic - Next topic

tilapisha

I was hoping to include having to solve the worldle (does not have to be the official one/ could be hard coded as one word) as the key to enter the next room.

I welcome any advice on how to do this, as I've just completed the AGS tutorial and am very much a beginner.

Much appreciated!  :grin:

Snarky

#1
Hmm… on second thought I think this proposal overlooks some key parts of Wordle, particularly that guesses have to be valid dictionary words. Let me get back to you…

Spoiler
Should be pretty easy. The hardest part is to deal with the whether to color a letter yellow, since the rules for that are a bit complicated (don't color yellow if the letter is already "taken" and occurs elsewhere in your guess as green or yellow, unless there are several of the same letter in the solution). Let's ignore that part for now.

(Also, I'm not doing "hard mode" where you have to use the letters you have already found. That's a bit more work.)

Let's set up some basic variables:

Code: ags
String correctWord="SNARK";
int currentGuess;


Now a function to check whether a letter in a guess is gray, yellow or green:

Code: ags
// 0=gray, 1=yellow, 2=green
int checkLetter(String guess, String solution, int i)
{
  if(guess.Chars[i]==solution.Chars[i]) return 2;
  if(solution.IndexOf(String.Format("%c",guess.Chars[i]))>=0) return 1;  // This is a simplification, doesn't check whether letter is "already taken"
  return 0;
}


Now let's say the player types their answer into a TextBox called inputText. When they press Enter, we run inputText_OnActivate():

Code: ags
function inputText_OnActivate(GUIControl* control)
{
  String guess = inputText.Text.UpperCase;
  guessedWords[currentGuess] = guess;
  if(guess.Length < 5) // Not enough letters, keep going
  {
    return;
  }
  if(guess.Length > 5) // Too many letters, ignore
  {
    inputText.Text = "";
    return;
  }
  if(guess == correctWord)
  {
    // TODO: Correct! Go to puzzle solved
  }
  else
  {
    // Show the feedback
    for(int=0; i<5; i++)
    {
      int col = checkLetter(guess, correctWord, i);
      // TODO: Display the letter in the appropriate color in the Wordle grid
    }
    currentGuess++;
    if(currentGuess>5)
    {
      // TODO: Out of turns! Go to puzzle failed
    }
  }
}


What's left to do now is just to display the Wordle grid to the player. One way to do that is on a GUI with a set of 5x6 buttons, where we set the Button.Graphic property to a sprite with the right background color and Button.Text to the letter.
[close]

Snarky

One thing to think about with this as an adventure game puzzle: what happens if the player fails the puzzle? If they cannot try again, then it must either be a death/game over (the player is caught by the security guards!), or you need some other way to get past this door. If they can simply try again, and the word is hardcoded, then it's essentially the same as having infinite guesses, and the puzzle becomes very easy. Or you need to make it so that the word changes each time.

tilapisha

I'm thinking using GUI's I could complete this.

Snarky

#4
Using GUIs for the output and input is certainly the easiest way, yes. The more interesting part is how to implement the rules of the game itself.

But to get you started: Create a GUI Window called WordleGUI with a 5x6 grid of equally sized square Buttons. This will be the Wordle grid. (We use buttons simply because they have the display features we need, not as something for the player to click on.) The code becomes much easier if you add the buttons in order (first button is the first letter of the first word, then the second letter and so on, and then the next row).

If you want to also have the feedback keyboard, you would make that out of Buttons as well, but I would suggest waiting to do that until you have the rest of it working.

Now let's link this GUI up to a basic representation of Wordle:

Code: ags
#define WORD_LENGTH 5
#define MAX_GUESSES 6

// This represents a row in the Wordle grid (IOW, a guess)
struct WordleGuess
{
   public Button* LetterField[WORD_LENGTH];
}

// This is our wordle grid: a set of Wordle rows
WordleGuess WordleGrid[MAX_GUESSES];

int currentGuess;
int currentInputLetter;

// This function just links up the buttons of the GUI with our Wordle grid
void SetupWordle()
{
  int control;
  for(int i=0; i<MAX_GUESSES; i++)
  {
    for(int j=0; j<WORD_LENGTH; j++)
    {
      WordleGrid[i].LetterField[j] = WordleGui.Controls[control].AsButton;
      control++;
    }
  }
}


You should run SetupWordle() on game start.
Now we can link the buttons up to keyboard input:

Code: ags
function on_key_press(eKeyCode keycode)
{
  if(WordleGui.Visible)
  {
    if(keycode >= eKeyA && keycode <= eKeyZ) // Typed a letter
    {
      if(currentInputLetter<WORD_LENGTH)
      {
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = String.Format("%c", keycode);
        currentInputLetter++;
      }
    }
    else if(keycode == eKeyBackspace) // Remove the last letter
    {
      if(currentInputLetter>0)
      {
        currentInputLetter--;
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = "";        
      }
    }
    else if(keycode == eKeyEnter)
    {
      if(currentInputLetter = WORD_LENGTH) // TODO: Check that word is a valid guess. Submit only if it is
      {
        // TODO: Check if guess is correct, update state of game
        currentGuess++;
        currentInputLetter=0;
    }
  }
}


This should let you type in guesses. It won't actually check what you've typed or anything, but once you have this set up you can start adding in the Wordle rules.

I would suggest doing the colors by having different sprites with the gray, yellow and green squares, and changing the Button.Graphic to the appropriate sprite.

tilapisha

This is so helpful! The wordle I'm going to use is XYZZY  :-D

Khris

Regarding the coloring of the guess:

Start with green and remove all matching letters from the guess and the solution (you can use an array to store the letters and replace them with a space character for instance).
Next, iterate over the remaining characters of the guess; if the character exists in the remaining characters of the solution, it's yellow. Again, remove the yellow match from both guess and solution. The remaining letters of the guess are grey.

tilapisha

#7
I'm setting up a prototype of this in a new game, trying to get this to work before implementing it in the actual game files. I'm having trouble implementing it. I'm going to hardcode the correct guess as "XYZZY".

Code: ags
// main global script file
int wordLength = 5;
int maxGuesses = 4;
 
// This represents a row in the Wordle grid (one guess)
struct WordleGuess
{
   public Button* Letterfield1[wordLength];
}
 
// This is our wordle grid: a set of Wordle rows
WordleGuess WordleGrid[maxGuesses];
 
int currentGuess;
int currentInputLetter;
 

// called when the game starts, before the first room is loaded
function game_start()
{

// This function just links up the buttons of the GUI with our Wordle grid
void SetupWordle()
{
  int control;
  for(int i=0; i<maxGuesses; i++)
  {
    for(int j=0; j<wordLength; j++)
    {
      WordleGrid[i].Letterfield1[j] = gWordle.Controls[control].AsButton;
      control++;
    }
  }
}
}

// called on every game cycle, except when the game is blocked
function repeatedly_execute()
{
}

// called on every game cycle, even when the game is blocked
function repeatedly_execute_always()
{
}

// called when a key is pressed
function on_key_press(eKeyCode keycode)
{
  if(WordleGui.Visible)
  {
    if(keycode >= eKeyA && keycode <= eKeyZ) // Typed a letter
    {
      if(currentInputLetter<wordLength)
      {
        WordleGrid[currentGuess].Letterfield1[currentInputLetter].Text = String.Format("%c", keycode);
        currentInputLetter++;
      }
    }
    else if(keycode == eKeyBackspace) // Remove the last letter
    {
      if(currentInputLetter>0)
      {
        currentInputLetter--;
        WordleGrid[currentGuess].Letterfield1[currentInputLetter].Text = "";        
      }
    }
    else if(keycode == eKeyEnter)
    {
      if(currentInputLetter = wordLength) // TODO: Check that word is a valid guess. Submit only if it is
      {
        // TODO: Check if guess is correct, update state of game
        currentGuess++;
        currentInputLetter=0;
    }
  }
}


On line 8, I'm getting an error of "expected variable type". I also made each button a text box button, and named them "letterfield1, lettefield2"...etc.



This is the GUI I've drawn, and I'm planning to put 20 buttons. So far, I'm working on the first row so only have 5 buttons.

Snarky

OK, so a few things about your code.

First, skip the public inside of WordleGuess. That's my mistake, I forgot you don't need it in AGS.

Second, you can't have wordLength and maxGuesses as int variables. Since they are used to set array lengths, they must be constants, so do it the way I showed in the example.

Third, you changed the name of the array from Letterfield to Letterfield1. AGS won't care either way, but this name is wrong. The array isn't just the first letter field, it is all of them.

Fourth, you have put the function SetupWordle() inside game_start(). That's not how functions work. You have to put them separately, and then you call SetupWordle() from game_start(). (Here's where I would recommend breaking up the Wordle code into a separate module, but we can do that later.)

Fifth, you say:

Quote from: tilapisha on Mon 11/04/2022 04:44:49
I also made each button a text box button, and named them "letterfield1, lettefield2"...etc.

So, I said to make them Buttons, not TextBoxes. A TextBox will not work for what we need. So delete them and start over, using Buttons. It won't look the way you want in the editor (or in-game initially), but we will be able to fix that later.

Also, while you can name them what you want, avoid using the names of any variables in the code. We will not be referring to these Buttons by name, but use the GUI.Controls[] array to loop through them. And, since this is a common misunderstanding about how names work, I'll point out that there's absolutely no way to use a pattern in the variable names like letterfield1, letterfield2, … to loop through them. IOW, you cannot do anything like this:

Code: ags
// WRONG!
for(int i=1; i<15; i++)
{
  Button* a = "letterfield" + i;
  a.Text = ""; 
}


The names are just labels, and they cannot be interpreted or manipulated in the code in any way. That's why we use the GUI.Controls[] array instead.

Which brings me to the final point. It's a nice drawing of the GUI, but I want to remind you that you should start only with the Wordle grid (that means not adding things like the label at the top yet). The code depends on the order in which you added things to the GUI, and only works if the Wordle grid buttons are the absolutely first things you added.

tilapisha

Gotcha, I appreciate all this info. I will instate it. As for the GUI, it's all a background image. So, the only thing on the GUI will be the buttons for the grid.

tilapisha

I've having some errors on line 71, and when I comment out the keycode Return else if statement (eKeyReturn instead of eKeyEnter, which throws an error), also line 25 .

71: Parse error with currentInputLetter, but I tried adding them to the Global variables. Still error.
25:  Will run the game, but with call stack error on 25 and 35.  "Error running function'game_start': and Null pointer referenced

I know basic programming in Python, HTML/CSS JavaScript and Java, but please forgive me as I do not know C++ well. Let me know if I should check out other resources before posting more on here, and I would also be very open to a zoom call or something of the sort if it's easier! <3 Lisha

Code: ags
// main global script file
#define WORD_LENGTH 5
#define MAX_GUESSES 6
 
// This represents a row in the Wordle grid (one guess)
struct WordleGuess
{
   Button* LetterField[WORD_LENGTH];
};
 
// This is our wordle grid: a set of Wordle rows
WordleGuess WordleGrid[MAX_GUESSES];
 
int currentGuess;
int currentInputLetter;

// This function just links up the buttons of the GUI with our Wordle grid
void SetupWordle()
{
  int control;
  for(int i=0; i<MAX_GUESSES; i++)
  {
    for(int j=0; j<WORD_LENGTH; j++)
    {
      WordleGrid[i].LetterField[j] = gWordle.Controls[control].AsButton;
      control++;
    }
  }
}


// called when the game starts, before the first room is loaded
function game_start()
{
SetupWordle();
}

// called on every game cycle, except when the game is blocked
function repeatedly_execute()
{
}

// called on every game cycle, even when the game is blocked
function repeatedly_execute_always()
{
}

// called when a key is pressed
function on_key_press(eKeyCode keycode)
{
   if(gWordle.Visible)
  {
    if(keycode >= eKeyA && keycode <= eKeyZ) // Typed a letter
    {
      if(currentInputLetter<WORD_LENGTH)
      {
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = String.Format("%c", keycode);
        currentInputLetter++;
      }
    }
    else if(keycode == eKeyBackspace) // Remove the last letter
    {
      if(currentInputLetter>0)
      {
        currentInputLetter--;
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = "";        
      }
    }
    else if(keycode == eKeyReturn)
    {
      if(currentInputLetter = WORD_LENGTH) // TODO: Check that word is a valid guess. Submit only if it is
      {
        //TODO: Check if guess is correct, update state of game
        currentGuess++;
        currentInputLetter=0;
      }
    }
  }
}

Khris

Code: ags
  if (currentInputLetter == WORD_LENGTH)


You need == for comparions.

tilapisha

Still throwing an error on lines 25 and 35 now. The null pointer, going to examine it carefully.

Khris

Did you create 30 buttons in your gWordle GUI?

tilapisha

It's working!

Code: ags
// main global script file
#define WORD_LENGTH 5
#define MAX_GUESSES 4
 
// This represents a row in the Wordle grid (one guess)
struct WordleGuess
{
   Button* LetterField[WORD_LENGTH];
};
 
// This is our wordle grid: a set of Wordle rows
WordleGuess WordleGrid[MAX_GUESSES];
 
int currentGuess;
int currentInputLetter;
 
// This function just links up the buttons of the GUI with our Wordle grid
void SetupWordle()
{
  int control;
  for(int i=0; i<MAX_GUESSES; i++)
  {
    for(int j=0; j<WORD_LENGTH; j++)
    {
      WordleGrid[i].LetterField[j] = gWordle.Controls[control].AsButton;
      control++;
    }
  }
}
// called when the game starts, before the first room is loaded
function game_start()
{
  // initialize gPanel controls
  initialize_control_panel();

  // set KeyboardMovement movement mode
  KeyboardMovement.Mode = eKeyboardMovementModeTapping;

  // set KeyboardMovement keys
  //KeyboardMovement.KeyUp = eKeyW;
  //KeyboardMovement.KeyDown = eKeyS;
  //KeyboardMovement.KeyLeft = eKeyA;
  //KeyboardMovement.KeyRight = eKeyD;
  
  SetupWordle();
  
}

// called on every game cycle, except when the game is blocked
function repeatedly_execute()
{
  cMallory.FollowCharacter(cGoby);
}

// called on every game cycle, even when the game is blocked
function repeatedly_execute_always()
{
}

// Called when a dialog script line "run-script" is processed
function dialog_request(int param)
{
}

function show_inventory_window()
{
  mouse.Mode = eModeInteract;
  open_gui(gInventory);
}

// called when a key is pressed
function on_key_press(eKeyCode keycode){
  
  if (IsGamePaused() || !IsInterfaceEnabled())
  {
    // game paused, so don't react to any keypresses
    keycode = 0;
  }
  else if (keycode == eKeyCtrlQ)
  {
    // Ctrl-Q will quit the game
    open_gui(gExitGame);
  }
  else if (keycode == eKeyF9)
  {
    // F9 will prompt to restart the game
    open_gui(gRestart);
  }
  else if (keycode == eKeyF12)
  {
    // F12 will save a screenshot to the save game folder
    SaveScreenShot("screenshot.pcx");
  }
  else if (keycode == eKeyCtrlS)
  {
    // Ctrl-S will give the player all defined inventory items
    Debug(0, 0);
  }
  else if (keycode == eKeyCtrlV)
  {
    // Ctrl-V will show game engine version and build date
    Debug(1, 0);
  }
  else if (keycode == eKeyCtrlA)
  {
    // Ctrl-A will show walkable areas
    Debug(2, 0);
  }
  else if (keycode == eKeyCtrlX)
  {
    // Ctrl-X will let the player teleport to any room
    Debug(3, 0);
  }
  else if (keycode == eKeyReturn)
  {
    if (gRestart.Visible)
    {
      // Enter confirms a restart
      RestartGame();
    }
  }
  else if (keycode == eKeyTab)
  {
    // Tab opens the inventory
    show_inventory_window();
  }
  
  if(gWordle.Visible)
  {
    if(keycode >= eKeyA && keycode <= eKeyZ) // Typed a letter
    {
      if(currentInputLetter<WORD_LENGTH)
      {
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = String.Format("%c", keycode);
        currentInputLetter++;
      }
    }
    else if(keycode == eKeyBackspace) // Remove the last letter
    {
      if(currentInputLetter>0)
      {
        currentInputLetter--;
        WordleGrid[currentGuess].LetterField[currentInputLetter].Text = "";        
      }
    }
    else if(keycode == eKeyReturn)
    {
      if(currentInputLetter == WORD_LENGTH) 
      {
        currentGuess++;
        currentInputLetter=0;
      }
      
      //if(WordleGrid.LetterField[0].Text == "x")
      //{
        //WordleGrid.LetterField[0].TextColor = 2016;
      //}
      //else if(WordleGrid.LetterField[0].Text=="y");
      //{
        //WordleGrid.LetterField[0].TextColor = 65510;
      //}
   
    }
  }
}


I'm now trying to get the buttons to change color, starting with textColor for testing.
Follow along here: https://github.com/lishap/gobyworld

tilapisha

Getting an error of an expected "[". Am I referencing the button correctly? You can also see this in context on my GitHub link. Thank you.

Code: ags
 else if(keycode == eKeyReturn)
    {
      if(currentInputLetter == WORD_LENGTH) 
      {
        currentGuess++;
        currentInputLetter=0;
      
        if(WordleGrid.LetterField[0].Text == "x")
        {
        WordleGrid.LetterField[0].TextColor = 2016;
        }
        else if(WordleGrid.LetterField[0].Text=="y" || WordleGrid.LetterField[0].Text=="z");
        {
        WordleGrid.LetterField[0].TextColor = 65510;
        }
        else
        {
          WordleGrid.LetterField[0].TextColor = 26;
        }
      }
    }

Snarky

#16
WordleGrid is itself an array, so you you have to pick one entry first (the current row). In other words, WordleGrid[currentGuess].LetterField[j].Text (where j is some index variable).

However, you are going down the wrong path here by hardcoding the test letter by letter. What you should be doing is writing a function that will test it in general, according to the Wordle rules. That will end up being simpler in the end.

I wrote a version of such a test in the first response, the checkLetter() function (which checks letter by letter), but Khris is right in his later post: it needs to be a function that treats the whole word together, because whether a letter is yellow depends on other letters in your guess. His suggested approach also seems sound.

I would implement it as a function String checkGuess(String guess, String answer), where the output is a String like "00120" (with 1 for yellow, 2 for green, 0 for gray). Or it can output an array of ints if you prefer. You give it the guess and the right answer, it gives you the pattern of the colors, and then you update the display accordingly. You can start off by just having it identify the green letters, since that's easy. Then work on doing the yellow ones as Khris suggests once you have that working.

The first thing I would do, though, is to update the WordleGuess struct with a ToString() method:

Code: ags

struct WordleGuess
{
   Button* LetterField[WORD_LENGTH];
   import String ToString();  // <-- Add this line to the existing struct
};

String WordleGuess::ToString()
{
  return String.Format("%s%s%s%s%s", this.LetterField[0].Text, this.LetterField[1].Text, this.LetterField[2].Text, this.LetterField[3].Text, this.LetterField[4].Text); 
}


This way you can easily get the word out of each row of the Wordle grid: String guess = WordleGrid[currentGuess].ToString();

Snarky

So, I did a little more on this. First, I added an enum to keep track of the field states, for formatting purposes:

Code: ags
enum WordleFieldState
{
  eWordleFieldBlank = 0,      // Nothing entered
  eWordleFieldEntry,          // Filled field in current row during editing
  eWordleFieldInvalid,        // Feedback that the word is invalid
  eWordleFieldGray,           // A gray field in a submitted row (letter not in word)
  eWordleFieldYellow,         // A yellow field in a submitted row (letter in word in other position)
  eWordleFieldGreen           // A green field in a submitted row (letter in word in same position)
};


Then I added a couple of functions to check your guess once it is submitted:

Code: ags
bool checkValid(String guess)
{
  // TODO: Just a placeholder. Need to add check against dictionary of valid guesses
  return true;
}

WordleFieldState[] checkGuess(String guess, String correctSolution)
{
  if(guess.Length != WORD_LENGTH || correctSolution.Length != WORD_LENGTH)
    AbortGame("Invalid Wordle data");
  WordleFieldState format[];
  format = new WordleFieldState[WORD_LENGTH];
  for(int i=0; i<WORD_LENGTH; i++)
  {
    if(guess.Chars[i] == correctSolution.Chars[i])
      format[i] = eWordleFieldGreen;
    // TODO: Replace this by something more complicated
    else if(correctSolution.IndexOf(String.Format("%c",guess.Chars[i])) >= 0)
      format[i] = eWordleFieldYellow;
    else
      format[i] = eWordleFieldGray;
  }
  return format;
}


Note that these functions are just placeholders! The second doesn't implement the full rules of when fields are marked yellow (because that's more complicated), and the first one doesn't really do anything at all, but it's where we would put the check for whether the word submitted is a valid guess or just "cnofi"-style nonsense.

Now we can put this into the on_key_press() logic:

Code: ags
    else if(keycode == eKeyReturn)
    {
      if(currentInputLetter == WORD_LENGTH) 
      {
        String guess = WordleGrid[currentGuess].ToString();
        if(guess.UpperCase == solution.UpperCase)
        {
          // TODO: Win!
        }
        if(checkValid(guess))
        {
          // Update state
          WordleFieldState formatRow[] = checkGuess(guess, solution);
          for(int i=0; i<WORD_LENGTH; i++)
          {
            switch(formatRow[i])
            {
              case eWordleFieldGreen:
                WordleGrid[currentGuess].LetterField[i].TextColor = Game.GetColorFromRGB(0, 128, 0);
                break;
              case eWordleFieldYellow:
                WordleGrid[currentGuess].LetterField[i].TextColor = Game.GetColorFromRGB(128, 128, 0);
                break;
              case eWordleFieldGray:
              default:
                WordleGrid[currentGuess].LetterField[i].TextColor = Game.GetColorFromRGB(64, 64, 64);
                break;
            }
          }
          currentGuess++;
          currentInputLetter=0;
          if(currentGuess >= MAX_GUESSES)
          {
            // TODO: Lose!
          }
        }
        else
        {
          // TODO: Feedback: invalid word
        }
      }
    }


Again, using text color for the feedback is just a placeholder. Later on we would change this to actually change the background color of the field.

tilapisha

It's working! I made this append to line 6,
Code: ags
   if(guess.UpperCase() == solution.UpperCase())
. Because the solution "XYZZY" is not a 'valid' guess in the real Wordle, I will not need to be checking for validity.

Right now, I have added
Code: ags
 String solution = "xyzzy";
above the String guess declaration. Right now, the guess "XYZZY" is giving a yellow output, but I am working on that.

This is what I've written under the win case so far, and WordleSolved boolean unlocks the ability to enter the next room.
Code: ags
 if(guess.UpperCase() == solution.UpperCase())
        {
          WordleSolved = true;
          gWordle.Visible() = false;
        }

Snarky

Glad it's working! (More or less. XYZZY should certainly not show up as yellow if that's the solution.)

Quote from: tilapisha on Wed 13/04/2022 16:27:04Because the solution "XYZZY" is not a 'valid' guess in the real Wordle, I will not need to be checking for validity.

"XYZZY" may not be a valid guess in actual Wordle, but if you don't have some list of words that are allowed as guesses, people will be able to type literally any random combination of letters. This, it seems to me, rather negates the point of the game. There's no reason to not just enter AEIOU, YTRNS, and so on. It removes the skill challenge.

In any case, I'm interested in a complete implementation of Wordle in AGS.

SMF spam blocked by CleanTalk