clubclue

Chess End Game Puzzle - In two weeks!

Chess End Game Puzzles

Thoughts Before Really Digging In

There will be, in total, three different points where Agnes will complete an end game move against the Mayor.

Ideally, I'd like to make 9 different versions of the puzzle - 3 pawn based, 3 queen based, and 3 knight based (mainly because I love knights the best).

The first plot point would rely on solving a pawn puzzle, the second plot point would rely on solving a queen puzzle, and the last would require a knight puzzle.

I'd like for the game to randomize which scenario out of three Agnes faces at each stage. So if the player tries to brute force the solution for the pawn puzzle, they would find that there are three different scenarios to track. I want to try and discourage brute forcing a solution as much as possible.

Board structure

The more elegant way of making this mini-game would be to utilize Godot's resourcing systems to change the chessboard based on data.

So the question would be what data structure to use to represent the board.

One maker on Youtube represented the board as an 8 by 8 matrix, using negative numbers ot represent one player, and positive numbers to represent the other player (https://youtu.be/1y57hJo1ONQ?list=PLd_56bdSJ-tS4-q1gczTdKJhqMep3Ij_w)

I think an idea like that could work, but working with matrices can be a real pain in terms of memory optimization goes.

For now, I will go with matrices to make the cleanest, most readable code I can for working with others. But if there are memory improvements to be made, I want to flag this area as an "easy" win. I think it'd be cool for this to run on my little netbook that could (1gb of Ram, 128gb of storage).

Pieces

Each piece should be set as a hotspot withing the EgoVenture framework. The one-button functionality of the game is built on the semantic structures of that framework.

Positive: Black Negative: White

Pawns: 1 or -1 Rooks: 2 or -2 Knights: 3 or -3 Bishop: 4 or -4 King: 5 or -5 Queen: 6 or -6

Scenarios

Initially, I considered that when pressed, each chess piece would display its possible moves, and each possible move would be an option for the player to select.

For a one-button game, however, this can be really cumbersome. In some situations, you could be sifting through 16 different buttons.

So instead, I think I will create another data structure per scenario - a list of possible options the user can pick from, similar to https://github.com/accesstechnology-mike/access-chess.

That way, The user will only really need to sort through, let's say, five options.

If the user selects the wrong one, the game will reset, and a new scenario will take it's place. So brute forcing will still be discouraged.

Below is a template resource for a scenario.

tool
class_name chess_template_resource
extends Resource

export(Array, Array, int) var board_data = [[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0]]

#0 is true, 1 is false, 2 is quit 
export var moves_dicts = {
    "move_1":1, 
    "move_2":0,
    "move_3":0, 
    "move_4":0,
    "move_5":0, 
}

Here, the dictionary maps the text descriptions of moves to whether or not they are the correct move. 0 represents correct, and 1 represents incorrect.

In the code's documentation, it is assumed that all scenarios have 5 options, and the scene will always have 6 buttons. The last button is reserved for the quit button. Based on whether a move is correct or not, the pressed signal of that button is connected to either the correct or incorrect functions.

#check if list of elements in the Vbox and the Scenarios is the same
    var menu_buttons = boxcontainer.get_children()
    var keys = scenario.moves_dicts.keys() 

    for i in menu_buttons.size():
        if i == menu_buttons.size()-1:
            menu_buttons[i].text = "Quit"
            break
        menu_buttons[i].text = keys[i]

        if scenario.moves_dicts[keys[i]] == 1:
            menu_buttons[i].connect("pressed", self, "correct")
        else:
            menu_buttons[i].connect("pressed", self, "incorrect")

Ordering pieces onto the board

<media-tag src="https://files.cryptpad.fr/blob/3c/3c7dae4f1ca95db27043552ffd5494334bfc706deca9c353" data-crypto-key="cryptpad:N+Ls+kNc5t52PhYnhFJDisnbKOPE/mveX2NmOM2e1t8="></media-tag>

Here is a quick sketch in Excalidraw showing how I envision the UI. I want to figure out how to make the procedurally generated sketch-y borders one day, but I am not a tech artist today. Perhaps one day I will be.

<media-tag src="https://files.cryptpad.fr/blob/a7/a75c45fb09699bc52e4f7950c56c9e28fa859a4e0dd3c150" data-crypto-key="cryptpad:J/lhQJ9GmSYGUCHcglM/oEZTRKfx8yCgbwLOByw/R98="></media-tag>

This is a rendered prototype of the game board in the simplest setting. The math of placing the chess pieces was honestly the hardest part. Because each chess piece has their origin point in the center, the spaces between them is the width/height of the board divided by 8. But, the board also has those gray offsets where the the letters and numbers help the player identify where pieces are!

I added a child node "offset" point to calculate the width and height of those boarders, and subtracted 2*offset from the base-width before dividing by 8.

But! we also need to start the first piece in the middle of a square! I achieved this by using another child Node2d called the starting point.

    var scenario = scenario_dictionary[0]

    var c = 0; 
    var r = 0; 
    var base_width = (board_sprite.scale.x*board_sprite.texture.get_width()-\
        offset_point.position.x*2*board_sprite.scale.x)/8
    var base_height =( board_sprite.scale.y*board_sprite.texture.get_height() \
        - offset_point.position.y*2*board_sprite.scale.y)/8

    for row in scenario.board_data:
        for cell in row: 

            if cell != 0: 
                var piece = load(piece_directory.get(cell)).instance()
                add_child(piece)
                piece.position.y = board_sprite.position.y + r*base_height + \
                    starting_point.position.y*board_sprite.scale.y
                piece.position.x = board_sprite.position.x +c*base_width +\
                    starting_point.position.x*board_sprite.scale.x
            c+=1
        r += 1
        c=0

Quit / Close Function

I changed the name of the Quit button to the Close button, to make it match other puzzles.

Thankfully, using the EgoVenture framework allows this process to be quite easy -- to leave the puzzle, I just need to call get_tree().change_scene(EgoVenture.previous_scene) when the Close button is pressed.

Shuffle!

I wanted the move options to shuffle every time the chess puzzle was opened as another way to discourage brute force solutions to the problem. At first I tried:

  var menu_buttons = boxcontainer.get_children()
    var keys = scenario.moves_dicts.keys().duplicate()
    keys.shuffle()

But I kept getting the same result for the shuffled array, no matter how many times I closed and opened the game.

I went digging into the documentation and realized: Godot's GDscript is one of the languages where you need to reset the randomization seed before it can give you a new, psuedorandom response. So now my code is:

    var menu_buttons = boxcontainer.get_children()
    var keys = scenario.moves_dicts.keys().duplicate()
    randomize() 
    #you need to reset the seed, or you might just get the same "shuffle"
    keys.shuffle()

Originally, I thought I wanted which puzzles the user interacted with to correspond to a specific plot point in the game, and thought I could do this by making each stage correspond to a different piece in chess. What I realized, though, was that if the player found out that this was an underlying pattern (especially because I'm writing a blog post about it!) then the jig was up and the puzzle becomes a lot easier.

So instead, I will keep a pool of 9 scenarios. If any puzzle is solved, they are taken out of the pool.

For the demo, this isn't necessary and I am on a deadline. So instead, I will prepare two scenarios and randomly select which one to display.

The little script below will choose which scenario to display. rnadi() is a random integer. Finding the remainer via the number of scenarios (size of the array) ensures that we will get no out of bounds errors.

randomize()
var scenario = scenarios[randi() % scenarios.size()]

Done!

Well, for now. I will need to complete more testing and integrate the mini-game into the larger game. But for something made in two weeks (and honestly, less than that given how busy I have been) I'm quite proud.

Want more? Be sure to check out our larger game!

Thoughts? Leave a comment