Chapter 8.P

Python MVC

MVC in Python

Subsections of Python MVC

A Worked Example - Model

Let’s continue our example exploring a program to play Connect Four by actually building the code to implement the game. By exploring the code, we should be able to better understand the structure and reasoning behind the MVC Architecture.

Problem Statement

First, let’s formalize our problem statement:

Write a program to play the traditional version of Connect Four. It should take place on a 6 x 7 game board.

On each turn, the game will print out the state of the board, then alert the current player to make a move. The player’s move will consist of a single number, giving the column in which to place a piece. If the move is invalid, the player will be given the option to select another move.

After each move is made, the game will determine if the current player has won, or if the game was a draw. To simplify the game, we will not check for diagonal wins.

The program should be built using MVC architecture.

So, for this program, we’ll use the following UML diagram:

Connect Four UML Diagram Connect Four UML Diagram

Let’s get started!

Model

YouTube Video

Video Materials

In many cases, the first part of any program following the MVC architecture to build is the model. This is usually the simplest and most straightforward part of the program, and both the view and the controller really depend on the model as the basis of the entire program. So, let’s start there.

First, we’ll need to build a class that contains the attributes and methods shown in the UML diagram above. By this point, we should be pretty used to this process:

class ConnectModel:

  @property
  def board(self):
    return self.__board
  
  @property
  def current_player(self):
    return self.__current_player
  
  # more code here

This code already contains implementations for the two property getter methods for the private attributes. So, all we have left to do is implement the constructor and a few methods.

Let’s focus on the constructor first. We can build the normal constructor as shown below:

def __init__(self, m, n):
  if m < 4 or n < 5:
    raise ValueError("The board must be at least 4 x 5")
  self.__board = []
  for i in range(0, m):
    self.__board.append([])
    for j in range(0, n):
      self.__board[i].append(0)
  self.__current_player = 1

This method really consists of two parts. First, we verify that the parameters provided are valid. If not, we’ll throw an exception. Then, it initializes the board attribute to the correct dimensions and fills it with empty squares represented by 0. We also set the current player to 1.

We also need to implement our load_game() method for testing, which simply accepts a board and a current player:

def load_game(self, board, current_player):
  if len(board) < 4:
    raise ValueError("Board must be at least 4 x 5")
  if len(board[0]) < 5:
    raise ValueError("Board must be at least 4 x 5")
  if current_player != 1 and current_player != 2:
    raise ValueError("Current player invalid - must be 1 or 2")
  self.__board = board
  self.__current_player = current_player

This method simply enforces the size of the board and the values that are possible for the current player, and then sets those values to the ones provided as arguments. As we discussed before, this method is primarily used for testing our model, and won’t actually be used in our program itself.

Once we have our constructors built, we can also build our make_move() method:

def make_move(self, y):
  if y < 0 or y >= len(self.board[0]):
    raise ValueError("Invalid column")
  if self.board[0][y] != 0:
    raise ValueError("Column full")
  row = len(self.board) - 1
  while self.board[row][y] != 0:
    row -= 1
  self.__board[row][y] = self.current_player
  return True

This method is also pretty straightforward. First, we use a couple of If-Then statements to check and see if the input is valid. If it is, then we simply iterate through the column chosen from the last row up to the first, looking for the first empty slot. Once we’ve found it, we place the current player’s piece there, and return True.

There we go! That’s a simple method for checking to see if a move in Connect Four is valid according to the rules of the game.

Next, we’ll need to write a method to determine if the current player has won. In this method, we’ll simply check to see if any row or column has at least 4 consecutive pieces for the current player.

def check_win(self):
  # check rows
  for i in range(0, len(self.board)):
    count = 0
    for j in range(0, len(self.board[0])):
      if self.board[i][j] == self.current_player:
        count += 1
      else:
        count = 0
      if count >= 4:
        return True
  
  # check columns
  for j in range(0, len(self.board[0])):
    count = 0
    for i in range(0, len(self.board)):
      if self.board[i][j] == self.current_player:
        count += 1
      else:
        count = 0
      if count >= 4:
        return True
      
  return False

This method is also pretty straightforward. We simply use two nested For loops to iterate across each row and down each column, keeping track of the count of items that match the current player. Anytime we see an item that doesn’t match, we reset our count to 0. If we reach 4 at any time, we can simply return True. If we reach the end of the method without returning True, then we know that a win condition hasn’t been reached and we should return False.

We’ll also need a method to check for a draw:

def check_draw(self):
  for j in range(0, len(self.board[0])):
    if self.board[0][j] == 0:
      return False
  return True

In this method, we only have to check the top row of the board. If any of them are empty, we can return False. However, if we find that all of them are filled, we can return True, indicating that the board is filled.

Finally, we’ll build one more method to switch players between 1 and 2:

def next_player(self):
  if self.current_player == 1:
    self.__current_player = 2
  else:
    self.__current_player = 1

It is really just a simple If-Then statement!

There we go! That should be everything we need to build a model of a working Connect Four game. We can use the assessments below to confirm that our code is working properly before continuing.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

Subsections of A Worked Example - Model

A Worked Example - View

The next part of any MVC program to build is the view. Depending on the complexity of the problem, sometimes it may be helpful to build the view and the model concurrently. Ideally, however, the model and the view should be properly distinct and separate, such that the model can be fully completed before the view is developed.

Let’s review our UML diagram before continuing:

Connect Four UML Diagram Connect Four UML Diagram

View

YouTube Video

The ConnectView class only contains a few simple methods. First, let’s build the class declaration and the constructor:

import sys

class ConnectView:
  
  def __init__(self):
    # default constructor
    pass

Since we aren’t initializing a GUI, we’ll just need a default constructor for this class.

Next, let’s look at the methods. First, we’ll need a method to show the board on the screen, called show_board(). Let’s see what it looks like:

def show_board(self, board):
  for row in board:
    for square in row:
      print("[{}]".format(square), end="")
    print("")

This method is very similar to methods we saw in an earlier module when we learned about arrays. Here, we are simply using two nested For loops to print the data in the list, with each row printed on its own line.

When we run this method, we should get output that looks like this:

[1][2][1][2][1]
[1][2][1][2][1]
[1][2][1][2][1]
[1][2][1][2][1]
[1][2][1][2][1]

The other complex method to implement is the show_menu() method, which reads input from the player:

def show_menu(self):
  while(True):
    try:
      print("Which column would you like to place a token in?")
      inp = sys.stdin.readline().strip()
      out = int(inp)
      return out
    except ValueError:
      print("Invalid input! Please enter a number")
    except Exception:
      print("Unable to read input")
      return -1

That method will print a prompt to the user, and get a response as a string. Notice that we aren’t doing any validation of the input here beyond making sure that it is an integer—we can do that in the controller and the model.

The final three methods simply print out messages to the user. So, we can implement those pretty easily:

def show_turn(self, player):
  print("It is player {}'s turn!".format(player))
  
def show_end_game(self, winner):
  if winner == 0:
    print("The game is a draw!")
  else:
    print("Player {} wins!".format(winner))
    
def show_error(self, error):
  print("Error: {}".format(error))

That’s it! The view portion of the program is very simple, but it allows our users to see what is going on and interact with the program. We can use the tests below to make sure our view is working correctly before we continue with the controller.

Web Only

This content is presented in the course directly through Codio. Any references to interactive portions are only relevant for that interface. This content is included here as reference only.

A Worked Example - Controller

Finally, we now can start working on our controller. This is where the program actually comes together and becomes useful. While a program such as Connect Four may seem quite complex, using MVC architecture can make it quite easy to work with. Let’s check it out!

Controller

YouTube Video

The ConnectController class just contains a single main() method:

from ConnectModel import *
from ConnectView import *
import sys

class ConnectController:
  
  @staticmethod
  def main(args):
    model = ConnectModel(6, 7)
    view = ConnectView()
    while True:
      view.show_board(model.board)
      view.show_turn(model.current_player)
      try:
        if model.make_move(view.show_menu()):
          if model.check_win() or model.check_draw():
            break
          model.next_player()
      except Exception as e:
        view.show_error(str(e))
    if model.check_win():
      view.show_end_game(model.current_player)
    else:
      view.show_end_game(0)

# main guard
if __name__ == "__main__":
  ConnectController.main(sys.argv)

This method uses methods in the view and model classes to get the game to work. For example, if we start inside of the while loop, we see the following steps:

  1. The game prints the current board to the screen.
  2. The game prints a message for the current user.
  3. The game then asks the user for input, and uses that input to make a move.
    1. If the move is successful, it checks to see if the game is over. If so, it exits the loop.
    2. If the move is unsuccessful, an exception is thrown and then caught and handled. The program repeats so the user has another opportunity to move.
  4. Finally, once the program exits the loop, it will print a message giving the outcome of the game.

That’s all there is to it! Once again, our main() method ends up being an outline of our program. In this case, almost every line of code is calling a method in both the model and the view, combining them in such a way as to make the program work seamlessly.

We can use the terminal to see how it works.