Skip to content

Games programming from the ground up with C: Validating and processing player moves

You can see an index of all the posts in this series: go to index.

If you want to review where I’ve got up to, you can download the code from the end of the previous post here: building a form

In the previous post I added code to handle forms. The first use for these was to enable the player to enter their move by typing in the numbers of the cells they want to move from and to. In this post I’ll add the code to validate the move entered by the player and then process it if it is valid.

One quick side note, as you’ll see from the screenshots featured in this post, I’ve changed the background colour of the board from green to magenta. I felt that the colour contrast between white and green wasn’t strong enough and some experimentation showed that magenta seemed to offer optimal contrast with both black and white.

The first thing I did was update the game loop in the play_game function in game.c, You can see this calls some new functions for handling the player moves and has placeholder comments for getting the AI move and switching players:

	hexmove_t player_move;

	// While game in progress
	while (current_game->state == STATE_IN_PROGRESS) {
			// if current player is white
			if (current_game->next_player == PIECE_WHITE) {
				player_move = get_player_move();
			} else {
				// TO DO: Get AI move
			}

			// If player has resigned...
			if (player_move.from == -1 && player_move.to == -1) {
				// ... set status to end game
				current_game->state = STATE_ENDED_RESIGNED;
			} else {
				// Make move
				move_piece(player_move.from, player_move.to);
			}
			
			// TO DO: If win condition 
				// Show win statement
				// End game
			// Else switch player
	}	

The first new function here is get_player_move on line 76. This waits for player input and then validates it before returning a valid move:

/**
 * @brief Get a player move
 * @returns A move entered by the player (will bet set to -1 -1 if the player is resigning)
*/
hexmove_t get_player_move() {
	int choice;
	hexmove_t move;
	bool valid_move = true;
	
	curs_set(1);		// Switch on the cursor

	display_prompt(STR_ENTER_MOVE);
	reset_form(FORM_PLAYER_MOVE, false);


	do {
		choice = navigation_dispatch(MENU_PLAY, FORM_PLAYER_MOVE);

		if (choice == PLAY_MOVE) {
			char * from_ptr = get_form_field_pointer(FORM_PLAYER_MOVE, 0);
			char * to_ptr = get_form_field_pointer(FORM_PLAYER_MOVE, 1);

			move.from = get_cell_number(from_ptr);
			move.to = get_cell_number(to_ptr);

			if (choice == PLAY_MOVE) {
				valid_move = validate_player_move(move.from, move.to);
			}
		}

	} while (!(choice == PLAY_EXIT || choice == PLAY_MOVE) || !valid_move);

	if (choice == PLAY_EXIT) {
		move.from = -1;
		move.to = -1;
	}

	curs_set(0); // Switch off the cursor

	return move;
}

This is similar to the code that was previously in the game loop, but now, on line 123 it calls a new function to validate the move. Note that an invalid move will prevent the input loop from exiting at line 127.

Also note that if the player exits the game, the move is set to from = -1 to = -1, which indicates a resignation. Later I will want to add some logic to ask the player if they want to resign or simply save the current game state.

Here’s the new function to validate the player move:

/** 
 * @brief Validates a move and shows an error message if the move is invalid
 * @param from The cell the player is trying to move from
 * @param to The cell the player is trying to move to
 * @returns True if the move is valid, False otherwise
 */
bool validate_player_move(int from, int to) {
	bool is_valid_move = false;
	int move_offset = from - to;
	int file = get_file(from);

	// Check move is within the board bounds
	if (from < 1 || to < 1 || from > 9 || to > 9) {
		display_prompt(STR_ENTER_VALID_MOVE);
	} else if (get_piece_at(from) != PIECE_WHITE) {
		// There is no white piece at the selected from cell
		display_prompt(STR_NO_WHITE);
	} else if (move_offset == 3) {
		// Player is moving forward one square
		piece_t piece_at_dest = get_piece_at(to);
		if (piece_at_dest == PIECE_WHITE) {
			display_prompt(STR_CELL_OCCUPIED);
		} else if (piece_at_dest == PIECE_BLACK) {
			display_prompt(STR_INVALID_CAPTURE);
		} else {
			is_valid_move = true;
		}
	} else if ((file == 1 && move_offset == 2) || (file == 2 && (move_offset == 2 || move_offset == 4)) || (file = 3 && move_offset == 4)) {
		// Player is moving diagonally
		if (get_piece_at(to) != PIECE_BLACK) {
			// Diagonal move doesn't lead to capture
			display_prompt(STR_DIAGONAL_NO_CAPTURE);
		} else {
			is_valid_move = true;
		}
	} else {
		// Player is attempting to move to an invalid position
		display_prompt(STR_INVALID_MOVE);
	}

	if (is_valid_move) {
		reset_form(FORM_PLAYER_MOVE, false);
	} else {
		reset_form(FORM_PLAYER_MOVE, true);
	}

	return is_valid_move;
}

This runs through the following checks:

  • Are the cell numbers within bounds?
  • Is there a white piece in the from cell?
  • Is the player trying to move forward one square, and if so is the cell being moved to unoccupied?
  • Is the player trying to move diagonally, and if so is the cell being moved to occupied by a black pawn?
  • Is the player trying to make a move other than those checked for above?

If any of the illegal conditions is found, the prompt is updated to show a suitable message to let the player know what they did wrong. The function then returns a boolean value to indicate whether the move was legal or not.

Screenshot from Hexapawn showing an example of a message about an invalid move.
An example of a prompt shown when the player enters an invalid move.

This function also makes use of a couple of new functions. get_file is used to get a number representing the file (i.e. the column) that a cell is in. It returns a number from 1 to 3 where 1 is the left file, 2 the middle file and 3 the right file:

/** 
 * @brief Returns the file for a given cell
 * @param cell The number of the cell to interrogate
 * @returns The file from 1(left) to 3(right) 
 */
int get_file(int cell) {
	int modulus = cell % 3;
	return modulus == 0 ? 3 : modulus; 
}

I need this information as valid diagonal moves depend on which file the player is moving from (i.e from file 1 the player can only move diagonally to the right, from file 2 to the left or right, and from file 3 only to the left). This is checked in line 182 of validate_player_move.

The second new function called by validate_player_move is get_piece_at. This simply returns the piece at the given cell in the current game:

/** 
 * @brief Returns the piece at a given cell in the current game
 * @param cell The number of the cell to interrogate
 * @returns The piece at the selected cell
 */
piece_t get_piece_at(int cell) {
	return current_game->board[cell - 1];
}

Once a valid move is returned to the play_game function, it calls another new function, move_piece, on line 87:

/** 
 * @brief Moves a piece
 * @param from The number of the cell to move from
 * @param to The number of the cell to move to
 */
void move_piece(int from, int to) {
	piece_t piece_to_move = get_piece_at(from);
	set_piece_at(from, PIECE_EMPTY);
	set_piece_at(to, piece_to_move);
	draw_piece(from, PIECE_EMPTY); 
	draw_piece(to, piece_to_move); 
}

This uses the existing draw_piece function to draw an empty piece in the from cell and then redraw the piece that originally occupied that cell in the destination cell. But first it calls the final new function, set_piece_at. As you might have guessed, this does the opposite of get_piece_at, setting a cell on the game board to the indicated piece type:

/** 
 * @brief Sets the piece at the given cell
 * @param cell The number of the cell to set the piece
 * @param piece The piece to set at that cell
 */
void set_piece_at(int cell, piece_t piece) {
	current_game->board[cell - 1] = piece;
}

With these new functions in place it’s now possible to move the white pieces around on the game board.

A screenshot from Hexapawn showing the board updated after the player has entered a valid move.
The player has entered a valid move and the board has been updated.

The next step is to add the logic for the computer player so that Black can respond. I’ll tackle this in the next post.

Published inGamesProgramming

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *