5
\$\begingroup\$

This time I wanted to make a board game that is very popular in Brazil, it's called 'resta um', it means 'there's only one left'. The idea of the game is that there are holes in which there are pieces inside, the player has to pick pieces and 'jump' one another until there is only one left on the board. For a move to be allowed, the selected piece must be one tile away from where it will 'jump' and the hole in which it will go has to be empty. It's kind of hard to explain it since I believe this game doesn't exist outside Brazil.

Download it here: https://github.com/Tlomoloko/Resta-Um/tree/master

This is the main code, I tried to separate the game-logic from the Tkinter representation of the board and stuff.

import tkinter as tk
import Resta_Um_Logic as game_logic

root = tk.Tk()

WIDTH = 600
HEIGHT = 600
BUTTON_WIDTH = 72
BUTTON_HEIGHT = 72
PIECE_IMAGE = tk.PhotoImage(file='restaumpiece.png')
HOLE_IMAGE = tk.PhotoImage(file='restaumhole.png')

canvas = tk.Canvas(root, width= WIDTH, height= HEIGHT, bg='#c5d1ed')
canvas.pack()

frame = tk.Frame(canvas, bg= '#e5e8ec')
frame.place(relx= 0.5, rely= 0.05, relwidth= 0.9, relheight= 0.9, anchor= 'n')
frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)

board_parts = [*game_logic.board]
game_buttons = []

for board_part in board_parts:
    if board_part.is_button:
        screen_button = tk.Button(frame, width=BUTTON_WIDTH, height=BUTTON_HEIGHT, bg='white', command=lambda x= board_part: click_button(x))
        screen_button.grid(row=board_part.row, column=board_part.column)
        board_part.button = screen_button
        game_buttons.append(board_part)
    else:
        empty_part = tk.Frame(frame, width= BUTTON_WIDTH, height= BUTTON_HEIGHT, bg='#e5e8ec')
        empty_part.grid(row=board_part.row, column=board_part.column)


selected_button = None
def click_button(clicked_button):
    global selected_button
    if not any(button.is_selected for button in game_buttons):
        clicked_button.is_selected = True
        selected_button = clicked_button
        selected_button.button.configure(relief= 'sunken')
    else:
        game_logic.check_moves(selected_button, clicked_button)
        selected_button.button.configure(relief= 'raised')
        for button in game_buttons:
            button.is_selected = False
        update_board()

def update_board():
    global game_buttons
    for button in game_buttons:
        if button.has_piece:
            button.button.configure(image= PIECE_IMAGE)
        else:
            button.button.configure(image= HOLE_IMAGE)

update_board()
root.mainloop()

Here I check if the 'selected' and 'targeted' pieces are valid.

class BoardPart():
    def __init__(self, row, column, button, has_piece, is_selected, is_button):
        self.row = row
        self.column = column
        self.button = button
        self.has_piece = has_piece
        self.is_selected = is_selected
        self.is_button = is_button

board = [BoardPart(0, 0, None, None, None, False), BoardPart(0, 1, None, None, None, False), BoardPart(0, 2, None, True, False, True), BoardPart(0, 3, None, True, False, True), BoardPart(0, 4, None, True, False, True), BoardPart(0, 5, None, None, None, False), BoardPart(0, 6, None, None, None, False),
         BoardPart(1, 0, None, None, None, False), BoardPart(1, 1, None, None, None, False), BoardPart(1, 2, None, True, False, True), BoardPart(1, 3, None, True, False, True), BoardPart(1, 4, None, True, False, True), BoardPart(1, 5, None, None, None, False), BoardPart(1, 6, None, None, None, False),
         BoardPart(2, 0, None, True, False, True), BoardPart(2, 1, None, True, False, True), BoardPart(2, 2, None, True, False, True), BoardPart(2, 3, None, True, False, True), BoardPart(2, 4, None, True, False, True), BoardPart(2, 5, None, True, False, True), BoardPart(2, 6, None, True, False, True),
         BoardPart(3, 0, None, True, False, True), BoardPart(3, 1, None, True, False, True), BoardPart(3, 2, None, True, False, True), BoardPart(3, 3, None, False, False, True), BoardPart(3, 4, None, True, False, True), BoardPart(3, 5, None, True, False, True), BoardPart(3, 6, None, True, False, True),
         BoardPart(4, 0, None, True, False, True), BoardPart(4, 1, None, True, False, True), BoardPart(4, 2, None, True, False, True), BoardPart(4, 3, None, True, False, True), BoardPart(4, 4, None, True, False, True), BoardPart(4, 5, None, True, False, True), BoardPart(4, 6, None, True, False, True),
         BoardPart(5, 0, None, None, None, False), BoardPart(5, 1, None, None, None, False), BoardPart(5, 2, None, True, False, True), BoardPart(5, 3, None, True, False, True), BoardPart(5, 4, None, True, False, True), BoardPart(5, 5, None, None, None, False), BoardPart(5, 6, None, None, None, False),
         BoardPart(6, 0, None, None, None, False), BoardPart(6, 1, None, None, None, False), BoardPart(6, 2, None, True, False, True), BoardPart(6, 3, None, True, False, True), BoardPart(6, 4, None, True, False, True), BoardPart(6, 5, None, None, None, False), BoardPart(6, 6, None, None, None, False)]

def check_moves(selected_hole, target_hole):
    hole_in_between = None
    if selected_hole.has_piece and not target_hole.has_piece and selected_hole.row == target_hole.row:
        if selected_hole.column - target_hole.column == 2:
            for hole in board:
                if hole.column == selected_hole.column - 1 and hole.row == selected_hole.row:
                    hole_in_between = hole
                    if hole_in_between.has_piece:
                        move_piece(hole_in_between, selected_hole, target_hole)
                    break
        elif selected_hole.column - target_hole.column == -2:
            for hole in board:
                if hole.column == selected_hole.column + 1 and hole.row == selected_hole.row:
                    hole_in_between = hole
                    if hole_in_between.has_piece:
                        move_piece(hole_in_between, selected_hole, target_hole)
                    break

    elif selected_hole.has_piece and not target_hole.has_piece and selected_hole.column == target_hole.column:
        if selected_hole.row - target_hole.row == 2:
            for hole in board:
                if hole.row == selected_hole.row - 1 and hole.column == selected_hole.column:
                    hole_in_between = hole
                    if hole_in_between.has_piece:
                        move_piece(hole_in_between, selected_hole, target_hole)
                    break
        elif selected_hole.row - target_hole.row == -2:
            for hole in board:
                if hole.row == selected_hole.row + 1 and hole.column == selected_hole.column:
                    hole_in_between = hole
                    if hole_in_between.has_piece:
                        move_piece(hole_in_between, selected_hole= selected_hole, target_hole= target_hole)
                    break

def move_piece(hole_in_between, selected_hole, target_hole):
    hole_in_between.has_piece = False
    selected_hole.has_piece = False
    target_hole.has_piece = True

I couldn't figure out a way to check if there were no more possible moves or if the player won the game(if there was only one piece left), so if the player loses or wins the program doesn't do anything. I still want to update the game graphics, because they're still rough sketches haha.

Hope you guys like the game!

\$\endgroup\$
  • 1
    \$\begingroup\$ The generic English name for it is "Peg solitaire": en.wikipedia.org/wiki/Peg_solitaire In the US it's mostly been marketed under the name "Hi-Q". \$\endgroup\$ – Sam Stafford Jan 16 at 16:17
  • \$\begingroup\$ Thanks, I'll edit it so people will know right away. \$\endgroup\$ – Tlomoloko Jan 16 at 16:28
4
\$\begingroup\$

There is room for improvement in your code, I'll give a few pointers of things that stand out to me.

DRY (Don't Repeat Yourself)

There is a lot of copied and pasted code in your work. It makes it harder to read and much harder to maintain.

To initialize the board, you manually put in 49 instances of a class with some parameters changing. What if you want to allow another empty space than the center one? Or a different shape of board? I Have seen a lot of variation around this game, and your solution is not flexible at all.

One option would be using list comprehensions:

board = [[not i == j == 3 if i in [2, 3, 4] or j in [2, 3, 4] else None for j in range(7)] for i in range(7)]

I'll admit it gets hard to read, and still isn't very flexible. I still think it is an improvement, as there are a lot less things to change if you want to change the starting board. This example fills a 7*7 array with None representing invalid spaces, True representing occupied spaces and False representing empty space.

Another method would be to parse a file containing a representation of the initial board, like:

..ooo..
..ooo..
ooooooo
oooxooo
ooooooo
..ooo..
..ooo..

You also copied and slightly modified code in the check_move method, which is not friendly to read.

Convoluted logic

The check_move method has 4 levels of nested if / elif / else statements, not counting the loops, making the program flow very hard to follow.

Separation of concerns

You said you tried to separate the game logic from the Tkinter representation. This is not really the case, for example your Tkinter buttons point to a BoardPart item which in turn contain a reference to the button.

It would be better to have a game object that can be called by the Tkinter model, or by any other model. Before adding the Tkinter UI, it can be nice to be able to test the game logic in the terminal. Yet your game logic isn't independent enough to allow that.

Redundant information

The BoardPart class contains useless info. I has a reference to the Tkinter buttons (it shouldn't, as stated before) and to the row and column of the piece (that info in contained in the index of the board array containing the object).

A BoardPart object really has 3 states: empty, with a peg, or it doesn't exist (in which case it can't have a peg). This is why I chose to use None, True and False in a 2-dimensional list to represent it in my earlier example. The indices of the list carry the row and column information

Documentation

Your code has no comments or docstrings. It makes it hard to understand for someone unfamiliar with it, and will make it hard to work on it again in the future.

Improvements

Your game runs fine. As you stated, it can be improved with a way to detect a winning or losing state, or by adding support for different starting boards.

Putting it all together

Here is my attempt at solving these issues. I did not work on a graphical part, but the game is functional and self-contained. It includes a __main__ guard allowing to run it directly and play in terminal, or import it from a Tkinter script (or for any other purpose).

class Game:
    """A simple peg solitaire game"""

    def __init__(self):
        """"Initialize the board when creating an instance"""
        self.board = [[not i == j == 3 if i in [2, 3, 4] or j in [2, 3, 4] else None for j in range(7)] for i in range(7)]


    def try_move(self, origin_x, origin_y, target_x, target_y):
        """If a move is valid from origin to target, move the peg and
        remove the peg in between from the board

        Does nothing if the move is invalid"""
        if not self._is_valid_move(origin_x, origin_y, target_x, target_y):
            return

        self.board[origin_x][origin_y] = False
        self.board[target_x][target_y] = True
        self.board[(origin_x + target_x) // 2][(origin_y + target_y) // 2] = False


    def _is_valid_move(self, origin_x, origin_y, target_x, target_y):

        if self.board[origin_x][origin_y] is none:
            return False   # origin is out of bounds
        if self.board[target_x][target_y] is none:
            return False   # target is out of bounds

        if not self.board[origin_x][origin_y]:
            return False   # origin is empty
        if self.board[target_x][target_y]:
            return False   # target is occupied

        # otherwise, if distance between origin and target is 2, return if
        # there is a peg between them
        if abs(origin_x - target_x) == 2 and origin_y == target_y:
            return self.board[(origin_x + target_x) // 2][origin_y]
        if abs(origin_y - target_y) == 2 and origin_x == target_x:
            return self.board[origin_x][(origin_y + target_y) // 2]

        return False   # not the right distance between orogin and target

    def check_win(self):
        """Returns True if a single peg is left on the board, 
        False otherwise"""
        peg_count = 0
        for row in self.board:
            for space in row:
                if space:
                    peg_count += 1
        return peg_count == 1

    def check_loss(self):
        """Not implemented

        The general approach would be to check if a valid move exists for
        all pegs remaining on the board"""
        return False 

    def print_board(self):
        """Print a simple representation on the board on the terminal"""
        for row in self.board:
            for space in row:
                if space is None:
                    print(" ", end="")
                elif space:
                    print("o", end="")
                else:
                    print("x", end="")
            print("")



if __name__ == '__main__':
    game = Game()
    while(not (game.check_win() or game.check_loss())):
        game.print_board()
        x0 = int(input('origin x\n'))
        y0 = int(input('origin y\n'))
        x1 = int(input('target x\n'))
        y1 = int(input('target y\n'))
        game.try_move(x0, y0, x1, y1)
    if game.check_win():
        print("Well Done")
    else:
        print("Try again")    
\$\endgroup\$
  • \$\begingroup\$ Totally agree with you, if I wanted to change the board in any way I would have to edit the characteristics of the object in the 'board' list, for example, if I wanted to change a hole with a piece into an empty one I would have to change it's 'has_piece'. That works but as you said is not very flexible. \$\endgroup\$ – Tlomoloko Jan 16 at 17:45
  • \$\begingroup\$ Next time I do something like this I'll try to do as you did, have a good 'game_logic' script and then implement all the graphical part. Thanks for that tip! \$\endgroup\$ – Tlomoloko Jan 16 at 17:46
  • \$\begingroup\$ Now just a last question, do you think I should keep learn about Tkinter if I want to work with UI's and stuff, or other programming languages/libraries are better? \$\endgroup\$ – Tlomoloko Jan 16 at 17:48
  • \$\begingroup\$ @tlomoloko If I went all the way to this project, I would have a default board self-contained in the class and the option to parse external resources (.txt file). \$\endgroup\$ – gazoh Jan 16 at 17:48
  • \$\begingroup\$ @tlomoloko I don't have much experience building GUIs, only a little bit with .NET WPF. It's fine, but since I can't compare it to anything else, I can't give a complete opinion on the matter. \$\endgroup\$ – gazoh Jan 16 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.