1
\$\begingroup\$

Please could you review my code below?

The game is similar to "Connect 4", but I haven't implemented diagonal checking for wins.

In particular I'm interested in my use of OOP. E.g.

My choice of classes

Did I need a Game class at all? I've tried to find out whether it's considered a good idea to have a class for game logic etc, but can't get an answer. I can see that here it could be considered overkill...

self.board used in both classes - probably a bad idea?

I'm also interested in feedback on Overall style and anything else which occurs to you.

import os
import random

LINE_LENGTH = 3
NUM_ROWS = 6
NUM_COLS = 7

EMPTY_SPACE = "-"
PLAYER_SYMBOL = "+"
COMPUTER_SYMBOL = "^"


class GameBoard:
    def __init__(self, rows=NUM_ROWS, cols=NUM_COLS, fill=EMPTY_SPACE):
        self.rows = rows
        self.cols = cols
        self.fill = fill
        self.board = [[fill] * cols for row in range(rows)]

    def __str__(self):
        return (
            "\n".join([" ".join(str(c) for c in row) for row in self.board])
            + "\n"
            + "-" * (2 * self.cols - 1)
            + "\n"
            + " ".join(str(col) for col in list(range(self.cols)))
            + "\n"
        )

    def is_free_position(self, col):
        for row in range(NUM_ROWS):
            if self.board[row][col] == EMPTY_SPACE:
                return True
        return False

    def insert_piece(self, col, symbol):
        for row_num in range(len(self.board) - 1, -1, -1):
            if self.board[row_num][col] == EMPTY_SPACE:
                self.board[row_num][col] = symbol
                return

    def is_win_position(self, board, piece):  # board is not instance variable
        # Horizontal check
        for row in range(NUM_ROWS):
            for col in range(NUM_COLS - (LINE_LENGTH - 1)):
                if all(
                    board[row][col + offset] == (piece) for offset in range(LINE_LENGTH)
                ):
                    return True
        # Vertical check
        for col in range(NUM_COLS):
            for row in range(NUM_ROWS - (LINE_LENGTH - 1)):
                if all(
                    board[row + offset][col] == (piece) for offset in range(LINE_LENGTH)
                ):
                    return True
        return False


class Game:
    def __init__(self):
        self.new_game()

    def print_title(self):
        print("X IN A LINE")
        print()

    def play_again(self):
        print()
        answer = input("Would you like to play again? ")
        print()
        return answer.lower().startswith("y")

    def new_game(self):
        self.board = GameBoard()
        self.turn = "player" if random.choice([0, 1]) == 0 else "computer"
        self.current_piece = PLAYER_SYMBOL if self.turn == "player" else COMPUTER_SYMBOL
        self.winner = None
        while self.winner is None:

            self.clear_scr()
            self.print_title()
            print(self.board)

            if self.turn == "player":
                self.player_plays()
            else:
                self.computer_plays()
            if self.board.is_win_position(self.board.board, self.current_piece):
                self.clear_scr()
                self.print_title()
                print(self.board)
                self.winner = self.turn
                print(
                    "Yay you won!"
                    if self.winner == "player"
                    else "Bad luck, the computer won."
                )
                if self.play_again():
                    self.new_game()
                else:
                    print("Goodbye.")
            # Switch player
            if self.turn == "player":
                self.turn = "computer"
                self.current_piece = COMPUTER_SYMBOL
            else:
                self.turn = "player"
                self.current_piece = PLAYER_SYMBOL

    def clear_scr(self):
        os.system("cls" if os.name == "nt" else "clear")

    def validate_input(self, player_choice):
        try:
            player_choice = int(player_choice)
        except ValueError:
            print("Input should be a number.")
            return False
        if 0 > player_choice or player_choice >= NUM_COLS:
            print("That number is out of range.")
            return False
        if not self.board.is_free_position(player_choice):
            print("That column is not available.")
            return False
        return True

    def player_plays(self):
        valid_input = False
        while not valid_input:
            player_choice = input("Enter a column number to place a piece: ")
            print()
            valid_input = self.validate_input(player_choice)
        self.board.insert_piece(int(player_choice), PLAYER_SYMBOL)

    def computer_plays(self):
        free_slot = False
        while not free_slot:
            computer_choice = random.randrange(NUM_COLS)
            if self.board.is_free_position(computer_choice):
                self.board.insert_piece(computer_choice, COMPUTER_SYMBOL)
                free_slot = True




game = Game()
\$\endgroup\$
  • \$\begingroup\$ A good way to cut down on solution checking in these kinds of problems (Sudoku solvers are similar) is to treat the board as a matrix. Here you could have a method for checking four horizontal counters in a row, then transform the board by rotation (90° for vertical, 45° for diagonal) and perform the same check again. Using numpy, these transformations are trivial. \$\endgroup\$ – QuantumChris Nov 13 at 11:52

Your Answer

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

Browse other questions tagged or ask your own question.