From c3e3dd94c05800ce1e6a049939e422f2dd34334e Mon Sep 17 00:00:00 2001 From: JS Ng Date: Fri, 11 Apr 2025 17:09:05 +0000 Subject: [PATCH 01/25] Add lesson 14 - abstraction patterns --- Programming in Python/lesson_14.ipynb | 604 ++++++++++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 Programming in Python/lesson_14.ipynb diff --git a/Programming in Python/lesson_14.ipynb b/Programming in Python/lesson_14.ipynb new file mode 100644 index 0000000..29e13d5 --- /dev/null +++ b/Programming in Python/lesson_14.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1189fcd9", + "metadata": {}, + "source": [ + "## 1-minute introduction to Jupyter ##\n", + "\n", + "A Jupyter notebook consists of cells. Each cell contains either text or code.\n", + "\n", + "A text cell will not have any text to the left of the cell. A code cell has `In [ ]:` to the left of the cell.\n", + "\n", + "If the cell contains code, you can edit it. Press Enter to edit the selected cell. While editing the code, press Enter to create a new line, or Shift+Enter to run the code. If you are not editing the code, select a cell and press Ctrl+Enter to run the code." + ] + }, + { + "cell_type": "markdown", + "id": "8d87d976", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e933773d", + "metadata": {}, + "source": [ + "# Lesson 14: Abstraction with Composition and Decomposition\n", + "\n", + "In this lesson, we further apply the principles of abstraction to an implementation of a Battleships game to see how it can be better organised for easier understanding and to minimise the likelihood of mistakes and bugs." + ] + }, + { + "cell_type": "markdown", + "id": "ac2fc210", + "metadata": {}, + "source": [ + "## Starting code\n", + "\n", + "A beginner programmer, after many hours, will usually have a working implementation that looks like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef7a34e1", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "# 1. Create the grid\n", + "board = []\n", + "for i in range(10):\n", + " row = []\n", + " for j in range(10):\n", + " row.append(\" \")\n", + " board.append(row)\n", + "\n", + "# 2. Ship details\n", + "ships = {\n", + " \"B\": 4, # Battleship\n", + " \"C\": 3, # Cruiser\n", + " \"D\": 2 # Destroyer\n", + "}\n", + "ship_locations = {} # To track each ship’s positions\n", + "\n", + "# 3. Function to place a ship\n", + "def place_ship(ship_name, size):\n", + " placed = False\n", + "\n", + " while not placed:\n", + " direction = random.choice([\"H\", \"V\"])\n", + " if direction == \"H\":\n", + " x = random.randint(0, 9)\n", + " y = random.randint(0, 10 - size)\n", + "\n", + " # Check for overlap\n", + " overlap = False\n", + " for i in range(size):\n", + " if board[x][y + i] != \" \":\n", + " overlap = True\n", + " if not overlap:\n", + " for i in range(size):\n", + " board[x][y + i] = ship_name\n", + " # Save ship location\n", + " ship_locations[ship_name] = []\n", + " for i in range(size):\n", + " ship_locations[ship_name].append((x, y + i))\n", + " placed = True\n", + "\n", + " else: # Vertical\n", + " x = random.randint(0, 10 - size)\n", + " y = random.randint(0, 9)\n", + "\n", + " overlap = False\n", + " for i in range(size):\n", + " if board[x + i][y] != \" \":\n", + " overlap = True\n", + " if not overlap:\n", + " for i in range(size):\n", + " board[x + i][y] = ship_name\n", + " ship_locations[ship_name] = []\n", + " for i in range(size):\n", + " ship_locations[ship_name].append((x + i, y))\n", + " placed = True\n", + "\n", + "# 4. Place all ships\n", + "for name in ships:\n", + " place_ship(name, ships[name])\n", + "\n", + "# 5. Game board to show to player\n", + "player_board = []\n", + "for i in range(10):\n", + " row = []\n", + " for j in range(10):\n", + " row.append(\" \")\n", + " player_board.append(row)\n", + "\n", + "# 6. Game loop\n", + "turns = 30\n", + "hits = 0\n", + "total_ship_cells = 4 + 3 + 2\n", + "\n", + "sunk_ships = []\n", + "\n", + "print(\"Welcome to Battleships!\")\n", + "while turns > 0 and hits < total_ship_cells:\n", + " print(\"\\nTurns left:\", turns)\n", + " print(\" \" + \" \".join(str(i) for i in range(10)))\n", + " for i in range(10):\n", + " print(i, \" \".join(player_board[i]))\n", + "\n", + " # 7. Player input\n", + " guess = input(\"Enter your guess (row,col): \")\n", + " if \",\" not in guess:\n", + " print(\"Invalid format. Use row,col\")\n", + " continue\n", + "\n", + " parts = guess.split(\",\")\n", + " if len(parts) != 2:\n", + " print(\"Please enter two numbers.\")\n", + " continue\n", + "\n", + " try:\n", + " row = int(parts[0])\n", + " col = int(parts[1])\n", + " except:\n", + " print(\"Please enter valid numbers.\")\n", + " continue\n", + "\n", + " if row < 0 or row >= 10 or col < 0 or col >= 10:\n", + " print(\"Coordinates out of range.\")\n", + " continue\n", + "\n", + " if player_board[row][col] != \" \":\n", + " print(\"You already guessed that!\")\n", + " continue\n", + "\n", + " # 8. Check hit or miss\n", + " if board[row][col] in ships:\n", + " ship_hit = board[row][col]\n", + " player_board[row][col] = \"X\"\n", + " hits += 1\n", + " print(\"Hit!\", ship_hit)\n", + "\n", + " # Remove that part from ship_locations\n", + " ship_locations[ship_hit].remove((row, col))\n", + " if len(ship_locations[ship_hit]) == 0 and ship_hit not in sunk_ships:\n", + " print(\"You sunk the\", ship_hit + \"!\")\n", + " sunk_ships.append(ship_hit)\n", + "\n", + " else:\n", + " print(\"Miss!\")\n", + " player_board[row][col] = \"O\"\n", + " turns -= 1\n", + "\n", + "# 9. End of game\n", + "print(\"\\nGame over!\")\n", + "if hits == total_ship_cells:\n", + " print(\"Congratulations! You sank all the ships!\")\n", + "else:\n", + " print(\"You ran out of turns!\")\n", + "\n", + "# 10. Reveal full board\n", + "print(\"\\nFinal board:\")\n", + "print(\" \" + \" \".join(str(i) for i in range(10)))\n", + "for i in range(10):\n", + " row = []\n", + " for j in range(10):\n", + " if board[i][j] in ships and player_board[i][j] == \" \":\n", + " row.append(board[i][j])\n", + " else:\n", + " row.append(player_board[i][j])\n", + " print(i, \" \".join(row))\n" + ] + }, + { + "cell_type": "markdown", + "id": "820d4dea", + "metadata": {}, + "source": [ + "The game is fine; it works after all ... for now? But if you come across a bug, or want to add a feature, and come back to this code after 6 months, it'll be a slog to try to understand what you were thinking when you wrote it. And that just makes you less motivated to fix that bug or add that feature ... is there a way to make it easier?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2320e702", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "22e5395f", + "metadata": {}, + "source": [ + "## Make smaller, composable functions\n", + "\n", + "The game code only has one function, and it's a huge one. If you come across a bug, it's hard to know where to begin bug-hunting. Instead, if we have smaller functions, it's easier to work through the logic of a single function.\n", + "\n", + "Let's break the code up into the following functions, each one representing a single task of the game:\n", + "\n", + "- `create_grid()`\n", + "- `place_ship()`\n", + "- `run_game()`\n", + "- `display_board()`\n", + "- `prompt_player_input()`\n", + "- `is_valid_input()`\n", + "- `is_valid_guess()`\n", + "- `prompt_valid_guess()`\n", + "- `get_enemy_guess()`\n", + "- `is_target_hit()`\n", + "- `targetting_update()`\n", + "- `player_update()`\n", + "- `is_gameover()`\n", + "- `display_final_boards()`\n", + "\n", + "Even before we put in any real code, we can see the naming of the functions is important. Naming things well helps us to understand what they do even before we dig into the documentation.\n", + "\n", + "The functions are even more explanatory once we put in the parameter names, types, and docstrings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9191a08d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "~ ~ ~ ~ ~\n", + "~ S S ~ ~\n", + "~ ~ ~ ~ ~\n", + "~ ~ ~ ~ ~\n", + "~ ~ ~ ~ ~\n", + "Hit!\n" + ] + } + ], + "source": [ + "def create_grid(n: int, char: str) -> list[list[str]]:\n", + " \"\"\"Create a grid of size n x n filled with char, and return it.\"\"\"\n", + " pass\n", + "\n", + "def place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", + " \"\"\"Place a ship of size size on the board at a random location.\"\"\"\n", + " pass\n", + "\n", + "def run_game(turns: int, ships: dict[str, int]) -> None:\n", + " \"\"\"Run the main game loop.\"\"\"\n", + " pass\n", + "\n", + "def display_board(board: list[list[str]]) -> None:\n", + " \"\"\"Display the current state of the board.\"\"\"\n", + " pass\n", + "\n", + "def is_valid_guess(board: list[list[str]], text: str) -> bool:\n", + " \"\"\"Check if the guess is valid. Return True if valid, False otherwise.\"\"\"\n", + " pass\n", + "\n", + "def prompt_valid_guess(board: list[list[str]]) -> tuple[int, int]:\n", + " \"\"\"Prompt the user for a valid guess.\n", + "\n", + " If the guess is invalid, keep prompting until a valid guess is entered.\n", + "\n", + " Return the row and column of the guess, as a tuple.\n", + " \"\"\"\n", + " pass\n", + "\n", + "def get_enemy_guess(board: list[list[str]]) -> tuple[int, int]:\n", + " \"\"\"Generate a random guess for the enemy.\n", + "\n", + " Return the row and column of the guess, as a tuple.\n", + " \"\"\"\n", + " pass\n", + "\n", + "def is_target_hit(board: list[list[str]], x: int, y: int) -> bool:\n", + " \"\"\"Check if the target hit a ship. Return True if it did, False otherwise.\"\"\"\n", + " pass\n", + "\n", + "def targetting_update(board: list[list[str]], hit_what: str, x: int, y: int) -> None:\n", + " \"\"\"Update the targetting board with the guess.\"\"\"\n", + " pass\n", + "\n", + "def player_update(board: list[list[str]], x: int, y: int) -> None:\n", + " \"\"\"Update the player board with the guess.\"\"\"\n", + " pass\n", + "\n", + "def is_gameover(turns: int, player_hits: int, enemy_hits: int, total_ship_cells: int) -> bool:\n", + " \"\"\"Check if the game is over. Return True if it is, False otherwise.\"\"\"\n", + " pass\n", + "\n", + "def display_final_boards(targetting: list[list[str]], board: list[list[str]]) -> None:\n", + " \"\"\"Display the final boards: targetting overlaid on the ship board.\"\"\"\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "c04ebd9c", + "metadata": {}, + "source": [ + "## Composing functions: Part 1\n", + "\n", + "Before we start to implement the functions, have a look at the *interface* of the functions—their parameter types and output types—and see if you are able to use them even before you see the code that implements them. For now, let's pretend they work perfectly.\n", + "\n", + "Take some time to actually do this before you scroll down to see the sample code.\n", + "\n", + "Really.\n", + "\n", + "Try it first.\n", + "\n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + ". \n", + "\n", + "Tried it?\n", + "\n", + "Okay, scroll down and see if this is what you had in mind:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "283cff49", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "# 2. Ship details\n", + "ships = {\n", + " \"B\": 4, # Battleship\n", + " \"C\": 3, # Cruiser\n", + " \"D\": 2 # Destroyer\n", + "}\n", + "\n", + "run_game(turns=30, ships={\"B\": 4, \"C\": 3, \"D\": 2})" + ] + }, + { + "cell_type": "markdown", + "id": "d72a5987", + "metadata": {}, + "source": [ + "Okay, that wasn't very clear, all the code ended up hidden in `run_game()` huh. Let's have a look inside `run_game()` then, since that's where the action happens:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65530dae", + "metadata": {}, + "outputs": [], + "source": [ + "# inside run_game(); pretend all the code below is indented by 4 spaces\n", + "# Variables for tracking game state\n", + "player_hits = 0\n", + "enemy_hits = 0\n", + "total_ship_cells = sum(ships.values())\n", + "\n", + "# Data structures for tracking game data\n", + "# For tracking player ships and damage\n", + "player_board = create_grid(10, \" \")\n", + "player_ship_cells = {} # To track each ship’s remaining cells\n", + "player_targetting = create_grid(10, \" \") # For tracking player guesses and hits\n", + "player_sunk_ships = []\n", + "\n", + "# Similarly for the enemy\n", + "enemy_board = create_grid(10, \" \")\n", + "enemy_ship_cells = {}\n", + "enemy_targetting = create_grid(10, \" \")\n", + "enemy_sunk_ships = []\n", + "\n", + "# Place ships on the boards\n", + "for name, size in ships.items():\n", + " place_ship(player_board, name, size)\n", + " player_ship_cells[name] = size\n", + " place_ship(enemy_board, name, size)\n", + " enemy_ship_cells[name] = size\n", + "\n", + "while not is_gameover(turns, player_hits, enemy_hits, total_ship_cells):\n", + " print(\"\\nTurns left:\", turns)\n", + " print(\"\\nPlayer's turn\")\n", + " display_board(player_targetting)\n", + " x, y = prompt_valid_guess(player_targetting)\n", + "\n", + " hit_char = enemy_board[x][y]\n", + " targetting_update(player_targetting, hit_char, x, y)\n", + " if is_target_hit(enemy_board, x, y):\n", + " player_hits += 1\n", + " enemy_ship_cells[hit_char].remove((x, y))\n", + " if len(enemy_ship_cells[hit_char]) == 0:\n", + " print(\"You sunk the\", hit_char + \"!\")\n", + " player_sunk_ships.append(hit_char)\n", + "\n", + " print(\"\\nEnemy's turn\")\n", + " display_board(player_board)\n", + " x, y = get_enemy_guess(player_board)\n", + "\n", + " hit_char = player_board[x][y]\n", + " targetting_update(enemy_targetting, hit_char, x, y)\n", + " if is_target_hit(player_board, x, y):\n", + " enemy_hits += 1\n", + " player_ship_cells[hit_char].remove((x, y))\n", + " if len(player_ship_cells[hit_char]) == 0:\n", + " print(\"Enemy sunk the\", hit_char + \"!\")\n", + " enemy_sunk_ships.append(hit_char)\n", + " turns -= 1\n", + "\n", + "# Game is over\n", + "if player_hits == total_ship_cells:\n", + " display_final_boards(player_targetting, enemy_board)\n", + " print(\"Congratulations! You sank all the enemy ships!\")\n", + "elif enemy_hits == total_ship_cells:\n", + " display_final_boards(enemy_targetting, player_board)\n", + " print(\"Game over! The enemy sank all your ships!\")\n", + "else:\n", + " print(\"Game over! You ran out of turns!\")" + ] + }, + { + "cell_type": "markdown", + "id": "93dccb19", + "metadata": {}, + "source": [ + "### Observations\n", + "\n", + "- We haven't even implemented any of the functions yet, but it's already easier to see at a glance the logic of the game and how it's going to work. Certainly more easily than with the first unabstracted version of the code.\n", + "\n", + "- In this abstracted code, there are no indexes. We avoid dealing with indexes when looking at \"high-level\" code, i.e. the general logic of the game, because indexes are a \"low-level\" detail, i.e. it delves into the details of how parts of the game work.\n", + "\n", + "- In high-level code, there are also very few `print()` function calls. Most of them are hidden in `display_*()` functions so as not to clutter up the code and obscure its high-level logic.\n", + "\n", + "- Breaking up a huge task into smaller tasks, each one handled by a single function, makes each individual function easier to reason about and implement.\n", + "\n", + "- Using functions to chunk logical blocks of code enables us to invoke repetitive operations without adding much more code. For example, we run many of the functions for both the player and the enemy, without having to duplicate the code. This reduces the chance of typos, and standardises operations in a way that makes debugging easier.\n", + "\n", + "- Despite the above benefits, we still notice some repetitive code, particularly within the game loop.\n", + "\n", + "## Decomposing functions\n", + "\n", + "Can we break down some of these tasks into even smaller functions? E.g. I'm noticing that `is_gameover()` is particularly huge, involving 4 different arguments.\n", + "\n", + "Thinking through its sub-operations, it needs to:\n", + "- check if player has won\n", + "- check if enemy has won\n", + "- check if we are out of turns\n", + "\n", + "We could thus further decompose the gameover check into:\n", + "\n", + "- `is_player_won()`\n", + "- `is_enemy_won()`\n", + "\n", + "The implementation of `is_gameover()` might look like:\n", + "\n", + "```python\n", + "def is_gameover(turns, player_hits, enemy_hits, total_ship_cells) -> bool:\n", + " return (\n", + " is_player_won(player_hits, total_ship_cells)\n", + " or is_enemy_won(enemy_hits, total_ship_cells)\n", + " or turns == 0\n", + " )\n", + "```\n", + "\n", + "As a side benefit, we can reuse these two functions further after the loop:\n", + "\n", + "```python\n", + "# Game is over\n", + "if is_player_won(player_hits, total_ship_cells):\n", + " display_final_boards(player_targetting, enemy_board)\n", + " print(\"Congratulations! You sank all the enemy ships!\")\n", + "elif is_enemy_won(enemy_hits, total_ship_cells):\n", + " display_final_boards(enemy_targetting, player_board)\n", + " print(\"Game over! The enemy sank all your ships!\")\n", + "else:\n", + " print(\"Game over! You ran out of turns!\")\n", + "```\n", + "\n", + "That doesn't seem like much of a benefit ... the line looks even longer now! But what we've done is reduce the amount of low-level code; if we later add more things to the game and need to modify the victory logic, we only need to change it in one place, the `is_player_won()` and `is_enemy_won()` functions (can we further abstract these two functions to one function?).\n", + "\n", + "### Another example\n", + "\n", + "What about `place_ship()`? The implementation for this was particularly long in the original code, due to having to handle horizontal and vertical cases separately.\n", + "\n", + "It could certainly be easier to read and debug if we split it up into `place_ship_horizontal()` and `place_ship_vertical()`. Sometimes there's a bug that only affects vertical but not horizontal placement; having two smaller functions makes it easier for us to narrow down the problem zone and eliminate contributing factors." + ] + }, + { + "cell_type": "markdown", + "id": "660f4d8f", + "metadata": {}, + "source": [ + "## Labelling constants by name (instead of value)\n", + "\n", + "If you return to the original code 6 months later, would you remember what `10` and `\" \"` in the original were for? Space strings (`\" \"`) are especially common in code, and it's easy to mix up what we are using them for (e.g. to space out the grid, but also to represent an empty grid)\n", + "\n", + "Let's label them instead. Many programming conventions ask programmers to state the constants upfront, near the top of the file, so let's do that:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc1f2106", + "metadata": {}, + "outputs": [], + "source": [ + "GRID_SIZE = 10\n", + "EMPTY = \" \"\n", + "HIT = \"X\"\n", + "MISS = \"O\"\n", + "\n", + "player_board = create_grid(10, EMPTY)" + ] + }, + { + "cell_type": "markdown", + "id": "ae90fa75", + "metadata": {}, + "source": [ + "It's easier to see what the second argument to `create_grid()` is for, isn't it? Now it's easy to see that the char is (most likely) used to fill the grid. And we also won't be left guessing if `'X'` represents a hit or miss." + ] + }, + { + "cell_type": "markdown", + "id": "f06531c7", + "metadata": {}, + "source": [ + "## Remaining problems\n", + "\n", + "Despite the above improvements, some problems remain:\n", + "\n", + "- It is tricky to keep track of the data for the player and the enemy. There's a targetting grid, a board grid, a list of sunk ships, a dict of ship cells, the number of hits, ... and possibly other variables that were introduced which slipped our notice.\n", + "- Different functions call for different data: `is_gameover()` requires the number of hits, `targetting_update()` requires the targetting grid, `is_valid_guess()` requires the board grid, and so on. \n", + "If only there was a way to **bundle up** player and enemy data separately, and simply pass that around to each function ...\n", + "- It is easy to make a typo and end up passing the wrong argument to a function, which could mess things up really badly and make things difficult to debug. What happens if we pass a board grid to `targetting_update()` accidentally? Or pass the player's targetting grid instead of the enemy's? Because the datatype is still the same—a list of lists—we wouldn't get an error, and instead get a confusing result, wasting lots of time debugging. \n", + "If only there was a way to **ensure correct data** is always passed ...\n", + "- Did you spot the bug in the original code? We access a cell in the grid using `grid[x][y]` notation, but because we display each inner list as a row, we should actually use `grid[y][x]`. Fixing this bug isn't the worst thing: it's making sure that the fix is applied everywhere in the code consistently. Missing out even one instance means we have a difficult-to-identify bug, especially if we thought we'd fixed it previously. Who wants to waste time chasing bugs? \n", + "Worse, this usage isn't intuitive; we are so used to referring to coordinates by `(x, y)` notation that we are sure to accidentally type `grid[x][y]` again at some point. \n", + "If only there was a way we could write (x, y) but have the indexing be done (y, x) instead ... could we **separate the interface from how it is implemented**?\n", + "\n", + "We'll explore one kind of programming that aims to address these issues in the next chapter." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 6d99e83a32c3f001b7e66bab12e25e62a2e58d2f Mon Sep 17 00:00:00 2001 From: JS Ng Date: Sat, 12 Apr 2025 07:01:30 +0000 Subject: [PATCH 02/25] [cleanup] clean up outputs and cells --- Programming in Python/lesson_14.ipynb | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Programming in Python/lesson_14.ipynb b/Programming in Python/lesson_14.ipynb index 29e13d5..9bed4dc 100644 --- a/Programming in Python/lesson_14.ipynb +++ b/Programming in Python/lesson_14.ipynb @@ -205,14 +205,6 @@ "The game is fine; it works after all ... for now? But if you come across a bug, or want to add a feature, and come back to this code after 6 months, it'll be a slog to try to understand what you were thinking when you wrote it. And that just makes you less motivated to fix that bug or add that feature ... is there a way to make it easier?" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "2320e702", - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", "id": "22e5395f", @@ -249,20 +241,7 @@ "execution_count": null, "id": "9191a08d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "~ ~ ~ ~ ~\n", - "~ S S ~ ~\n", - "~ ~ ~ ~ ~\n", - "~ ~ ~ ~ ~\n", - "~ ~ ~ ~ ~\n", - "Hit!\n" - ] - } - ], + "outputs": [], "source": [ "def create_grid(n: int, char: str) -> list[list[str]]:\n", " \"\"\"Create a grid of size n x n filled with char, and return it.\"\"\"\n", From 6098332465b6d394de9b6db43fc4438e1c4cd128 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Sat, 12 Apr 2025 07:13:13 +0000 Subject: [PATCH 03/25] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 63a6904..3b5b067 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,7 @@ The following topics are covered: - importing modules - `__name__` - aliasing +14. Lesson 14: Abstraction with Composition and Decomposition [X] + - composable functions + - composing and decomposing functions + - using constants From 0c45ae2b1b6fb1a10ea5716d0bfb8cfe3bce44bc Mon Sep 17 00:00:00 2001 From: JS Ng Date: Sat, 12 Apr 2025 07:13:42 +0000 Subject: [PATCH 04/25] minor edits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b5b067..4f8d204 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ The following topics are covered: - importing modules - `__name__` - aliasing -14. Lesson 14: Abstraction with Composition and Decomposition [X] +14. Abstraction with Composition and Decomposition [X] - composable functions - composing and decomposing functions - using constants From e58c77f161b0559d60c492e29ced8a67bea010b0 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Sat, 12 Apr 2025 17:19:09 +0000 Subject: [PATCH 05/25] add lesson files for OOP --- .../01 Encapsulation.ipynb | 610 ++++++++++++++++++ .../02 Inheritance.ipynb | 322 +++++++++ .../03 Polymorphism.ipynb | 248 +++++++ Object-Oriented Programming/starting_code.py | 245 +++++++ README.md | 11 + 5 files changed, 1436 insertions(+) create mode 100644 Object-Oriented Programming/01 Encapsulation.ipynb create mode 100644 Object-Oriented Programming/02 Inheritance.ipynb create mode 100644 Object-Oriented Programming/03 Polymorphism.ipynb create mode 100644 Object-Oriented Programming/starting_code.py diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb new file mode 100644 index 0000000..13240d7 --- /dev/null +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -0,0 +1,610 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1189fcd9", + "metadata": {}, + "source": [ + "## 1-minute introduction to Jupyter ##\n", + "\n", + "A Jupyter notebook consists of cells. Each cell contains either text or code.\n", + "\n", + "A text cell will not have any text to the left of the cell. A code cell has `In [ ]:` to the left of the cell.\n", + "\n", + "If the cell contains code, you can edit it. Press Enter to edit the selected cell. While editing the code, press Enter to create a new line, or Shift+Enter to run the code. If you are not editing the code, select a cell and press Ctrl+Enter to run the code." + ] + }, + { + "cell_type": "markdown", + "id": "8d87d976", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e933773d", + "metadata": {}, + "source": [ + "# Object-Oriented Programming\n", + "\n", + "This lesson begins a new chapter on **Object-Oriented Programming**, a different paradigm of programming. Up to this point we have been learning imperative programming, a style of programming that focuses on writing line after line of commands to the computer, repeating operations with loops, selecting operations with branching, and chunking operations with procedures (which we treat as functions).\n", + "\n", + "In lesson 14, despite applying abstraction as best as we can, we are still left with a number of challenges. Let's see how object-oriented programming, a different way of thinking about code organisation, can help address some of them." + ] + }, + { + "cell_type": "markdown", + "id": "a241cd13", + "metadata": {}, + "source": [ + "## Object-Oriented Programming Principles\n", + "\n", + "Object-Oriented Programming (OOP) is centred around three principles: **Encapsulation**, **Polymorphism**, and **Inheritance**. We will cover one principle in each lesson, beginning with some code, explaining how the principle addresses some challenges, and demonstrating how it is applied to the code.\n", + "\n", + "**Note:** Many websites will mention a fourth principle, Abstraction. This principle is not mentioned in the 9569 syllabus, and here will be treated as a more general principle that applies beyond OOP." + ] + }, + { + "cell_type": "markdown", + "id": "848c2dde", + "metadata": {}, + "source": [ + "### Starting code\n", + "\n", + "Following on from lesson 14, and applying the improvements discussed, we have our starting code in `starting_code.py`. It is not displayed here so as to keep the lesson document short, so take a pause at this point to have a look first before we proceed.\n", + "\n", + "[Open starting_code.py](starting_code.py)\n", + "\n", + "It's 245 lines of code, seems to be working fine but not thoroughly tested, so don't be surprised to find bugs!\n", + "\n", + "Can we make it even more readable, perhaps shorter, and even maybe find some bugs along the way?" + ] + }, + { + "cell_type": "markdown", + "id": "53bdcba3", + "metadata": {}, + "source": [ + "## Encapsulation\n", + "\n", + "One of the challenges mentioned in [Lesson 14](../Programming%20in%20Python/lesson_14.ipynb) is that even with the function parameters labelled and annotated, it's easy to mix them up. We have two kinds of boards here, a targetting board that tracks hits and misses, and a playing board that tracks ship positions.\n", + "\n", + "(_We could combine the two into one board, but didn't because that would make it harder to show the players a targetting board without also showing the opponent's ships._)\n", + "\n", + "For example:\n", + "\n", + "```python\n", + "def display_overlay(targetting: list[list[str]], board: list[list[str]]) -> None:\n", + "```\n", + "\n", + "Even with this function signature, it's possible, even easy, to accidentally swap the targetting board and playing board. Worse, we might swap the player and enemy boards!\n", + "\n", + "Furthermore, there is information that is closely related to the board, such as the `GRID_SIZE`, and the `player_ship_cells` dict that keeps track of the number of cells remaining for each ship. It would be all too easy to update one of these items and forget to update other items.\n", + "\n", + "We can make it easier to access these items together by **bundling** them into an **object**.\n", + "\n", + "### Bundling data\n", + "\n", + "#### Bundling with dicts\n", + "\n", + "In python, we've seen that dicts can be used to bundle data. For example, we could create a board as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0239b757", + "metadata": {}, + "outputs": [], + "source": [ + "from starting_code import GRID_SIZE, create_grid\n", + "board = {\n", + " \"grid\": create_grid(GRID_SIZE, \"~\"),\n", + " \"grid_size\": GRID_SIZE\n", + "}\n", + "board" + ] + }, + { + "cell_type": "markdown", + "id": "bb3ace8f", + "metadata": {}, + "source": [ + "But this still does not solve the problem of accidentally swapping targetting and playing boards.\n", + "\n", + "#### Bundling with objects\n", + "\n", + "We could create an **object**. We can bind variables to an object; such variables are called **attributes**.\n", + "\n", + "To do so, we need to create a **class**. A class is a blueprint for creating objects. In technical terms, we say that:\n", + "\n", + "- a class is used to **instantiate** an object, OR\n", + "- an object is an instance of a class\n", + "\n", + "Think of a rubber stamp. We can use it (with an inkpad) to stamp patterns of different colours. But the stamped image is different from the rubber stamp. Likewise, objects are instances of their classes, but are not the same as their class.\n", + "\n", + "Let's create a `Grid` class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47074496", + "metadata": {}, + "outputs": [], + "source": [ + "# the `class` keyword is used to define a class\n", + "class Grid:\n", + " pass\n", + "\n", + "board = Grid()\n", + "board.grid = create_grid(GRID_SIZE, \"~\")\n", + "board.grid_size = GRID_SIZE\n", + "\n", + "print(\"board.grid attribute:\", board.grid)\n", + "print(\"board.grid_size attribute:\", board.grid_size)\n", + "grid" + ] + }, + { + "cell_type": "markdown", + "id": "8ea5a13b", + "metadata": {}, + "source": [ + "Notice that trying to access `grid` and `grid_size` directly doesn't work; they are attributes of `board` and must be accessed from `board` using the `object.attribute` notation (e.g. `board.grid` and `board.grid_size`), not as variables.\n", + "\n", + "It is annoying to have to write 3 lines of code to create a prepared grid though, so we would usually use a _factory function_, which are functions used to create objects:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11b3caf0", + "metadata": {}, + "outputs": [], + "source": [ + "def create_board(grid_size: int) -> Grid:\n", + " board = Grid()\n", + " board.grid = create_grid(grid_size, \"~\")\n", + " board.grid_size = grid_size\n", + " return board\n", + "\n", + "board = create_board(GRID_SIZE)\n", + "board" + ] + }, + { + "cell_type": "markdown", + "id": "ad441d9c", + "metadata": {}, + "source": [ + "Notice that Python recognises `board` as an instance of `Grid`; we can use `isinstance()` to check this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "700e9520", + "metadata": {}, + "outputs": [], + "source": [ + "isinstance(board, Grid)" + ] + }, + { + "cell_type": "markdown", + "id": "a59065f0", + "metadata": {}, + "source": [ + "Python offers an even easier way to create a prepared object: instead of having to write a separate factory function (which is also easy to mix up with other factory functions), it lets us **bundle the factory function into the class**. Such bundled factory functions are called **constructor methods**. Just as an attribute is a variable bound to an object, a **method** is a function bound to an object. Attributes and methods must be accessed through the object, using `object.attribute` and `object.method()` syntax.\n", + "\n", + "In Python, we define a constructor method as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff79dbbe", + "metadata": {}, + "outputs": [], + "source": [ + "from starting_code import EMPTY\n", + "\n", + "class Grid:\n", + " \"\"\"Represents a grid for a game board.\n", + " \n", + " In Python, a class can have a docstring as well. This docstring is added immediately\n", + " after the class definition.\n", + " \"\"\"\n", + " def __init__(self, grid_size: int, char: str = EMPTY):\n", + " \"\"\"__init__() is the constructor method for the class.\n", + " This special name is determined by Python and cannot be changed.\n", + " In a Python method definition, the first argument is always `self`, which refers\n", + " to the instance that is being created.\n", + " (It need not be named `self`, but it is a Python convention to do so.)\n", + " To access or set the variables of the object being created, we do so from `self`.\n", + " \"\"\"\n", + " self.grid = create_grid(grid_size, char)\n", + " self.grid_size = grid_size\n", + " # Note that no return statement is required.\n", + " # Be careful not to write:\n", + " # grid = ...\n", + " # This would create a new local variable `grid` that is not accessible outside\n", + " # the constructor method, instead of setting the instance variable `self.grid`.\n", + "\n", + "# A class with a constructor method can be instantiated like this:\n", + "board = Grid(GRID_SIZE)\n", + "# Note that we do not need to pass `self` as an argument to the constructor method.\n", + "# Python automatically passes the instance as the first argument to the constructor method.\n", + "board" + ] + }, + { + "cell_type": "markdown", + "id": "7c67646a", + "metadata": {}, + "source": [ + "### Using objects: `place_ship()`\n", + "\n", + "Let's see what it is like refactoring the code to use an object. **Refactoring** refers to the process of changing the structure of code without changing its behaviour. We will refactor the `place_ship()` function to use the `Grid` class we just created.\n", + "\n", + "#### Before\n", + "\n", + "```python\n", + "ef place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", + " \"\"\"Place a ship of size size on the board at a random location.\"\"\"\n", + " orientation = random.choice([HORIZONTAL, VERTICAL])\n", + " placed = False\n", + " while not placed:\n", + " row = random.randint(0, GRID_SIZE - 1)\n", + " col = random.randint(0, GRID_SIZE - 1)\n", + " if orientation == HORIZONTAL and col + size <= GRID_SIZE:\n", + " if is_unoccupied(board, row, col, size, HORIZONTAL):\n", + " place_ship_horizontally(board, ship_name, size, row, col)\n", + " placed = True\n", + " elif orientation == VERTICAL and row + size <= GRID_SIZE:\n", + " if is_unoccupied(board, row, col, size, VERTICAL):\n", + " place_ship_vertically(board, ship_name, size, row, col)\n", + " placed = True\n", + "```\n", + "\n", + "#### After\n", + "\n", + "```python\n", + "def place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", + " \"\"\"Place a ship of size size on the board at a random location.\"\"\"\n", + " orientation = random.choice([HORIZONTAL, VERTICAL])\n", + " placed = False\n", + " while not placed:\n", + " row = random.randint(0, board.grid_size - 1)\n", + " col = random.randint(0, board.grid_size - 1)\n", + " if orientation == HORIZONTAL and col + size <= board.grid_size:\n", + " if is_unoccupied(board, row, col, size, HORIZONTAL):\n", + " place_ship_horizontally(board, ship_name, size, row, col)\n", + " placed = True\n", + " elif orientation == VERTICAL and row + size <= board.grid_size:\n", + " if is_unoccupied(board, row, col, size, VERTICAL):\n", + " place_ship_vertically(board, ship_name, size, row, col)\n", + " placed = True\n", + "```\n", + "\n", + "Of course, we would need to refactor other functions to also use the `Grid` class, but we won't do that yet.\n", + "\n", + "... That seems underwhelming; all we did was eliminate the use of `GRID_SIZE` in the function. But encapsulation is not only applied to bundle data together; it also allows us to bundle related functions into the class.\n", + "\n", + "## Bundling methods\n", + "\n", + "For example, `is_unoccupied(board, row, column, size, orientation)` is a function that checks if a ship can be placed at a given location. It is closely related to the `Grid` class, so we can bundle it into the class as a method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ecddd83", + "metadata": {}, + "outputs": [], + "source": [ + "from starting_code import HORIZONTAL, VERTICAL, EMPTY\n", + "\n", + "class Grid:\n", + " \"\"\"Represents a grid for a game board.\"\"\"\n", + " def __init__(self, grid_size: int, char: str = EMPTY):\n", + " \"\"\"For commonly understood methods like __init__, it is fine to leave out docstrings.\"\"\"\n", + " self.grid = create_grid(grid_size, char)\n", + " self.grid_size = grid_size\n", + "\n", + " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", + " \"\"\"Check if the specified area on the board is unoccupied.\n", + "\n", + " Return True if unoccupied, False otherwise.\n", + "\n", + " (don't forget to add the `self` parameter when converting a function to the method!)\n", + " (also notice that `self` has replaced the `board` parameter, which is no longer needed)\n", + " \"\"\"\n", + " if orientation == HORIZONTAL:\n", + " for i in range(size):\n", + " if self.grid[row][col + i] != EMPTY:\n", + " return False\n", + " elif orientation == VERTICAL:\n", + " for i in range(size):\n", + " if self.grid[row + i][col] != EMPTY:\n", + " return False\n", + " return True\n", + "\n", + "board = Grid(GRID_SIZE)\n", + "board.is_unoccupied(0, 0, 3, HORIZONTAL)" + ] + }, + { + "cell_type": "markdown", + "id": "da32b921", + "metadata": {}, + "source": [ + "It's nicer to write `board.is_unoccupied(row, col, size, HORIZONTAL)` than `is_unoccupied(board, row, col, size, HORIZONTAL)`, isn't it? Better still, we will never pass the wrong board to the method, because the board isn't even a parameter of the method; it is guaranteed to act on the correct board." + ] + }, + { + "cell_type": "markdown", + "id": "385f66fc", + "metadata": {}, + "source": [ + "### Exercise: bundling data and methods\n", + "\n", + "Practise applying what you just learnt. Bundle the following methods into the `Grid` class:\n", + "- `place_ship_horizontally()`\n", + "- `place_ship_vertically()`\n", + "- `place_ship()`\n", + "- `display_board()` (rename as `display()`)\n", + "\n", + "You may copy the code from the previous cell and continue working from there." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e0a758f", + "metadata": {}, + "outputs": [], + "source": [ + "from starting_code import HORIZONTAL, VERTICAL, EMPTY\n", + "\n", + "# Define and implement your `Grid` class below.\n" + ] + }, + { + "cell_type": "markdown", + "id": "94728653", + "metadata": {}, + "source": [ + "\n", + "### Separating interface from implementation\n", + "\n", + "Besides bundling, we can also design methods in a way that let us separate the interface from the implementation.\n", + "\n", + "When we use Python functions, such as `random.randint()` or `divmod()`, notice that we don't actually know *how* they work. By reading the documentation, we are told what argument types they expect, what they return, and what they do. And that is enough for us to use them. We don't need to know how they are implemented.\n", + "\n", + "The **interface** is the part of the code that is visible to the user: for us, that's usually the function/method parameter types, the return type, and the docstring.\n", + "\n", + "The **implementation** is the part of the code that is hidden from the user: for us, that is the inner workings of the function/method, i.e. the \"code\".\n", + "\n", + "A good class design makes it possible to use the class without needing to know how it works. Our `Grid` class does not qualify yet, because many parts of the code rely on its attributes being a specific type. For example, in the `Grid.is_unoccupied()` method, we have the following line:\n", + "\n", + "```python\n", + "if self.grid[row][col + i] != EMPTY:\n", + "```\n", + "\n", + "Notice this relies on `self.grid` being a list of lists. This seems fine and natural to you at the moment, because you can't imagine other ways of implementing a 2D grid; surely it has to be a list of lists?!\n", + "\n", + "But as you improve in skill, or as you add more functionality to the game such that a list of list of `str`s is no longer sufficient, we'll need to change the grid format. But doing this will require carefully inspecting the code, checking every place where we treated `Board.grid` as a list of lists, and changing it to the new format. This is error-prone and tedious.\n", + "\n", + "How would we design the interface of the `Grid` class to improve this? In classic OOP, we treat most attributes as **private**. This means that they are not directly accessible from outside the class. Instead, we provide **getter** and **setter** methods to access and modify them. In programming languages where this is enforced, such as Java, our `Grid` class would look like this:\n", + "\n", + "```python\n", + "class JavaGrid:\n", + " def __init__(self, grid_size: int, char: str) -> None:\n", + " self._grid = create_grid(grid_size, char)\n", + " self._grid_size = grid_size\n", + " \n", + " def get_grid(self) -> list[list[str]]:\n", + " return self._grid\n", + "\n", + " def set_grid(self, grid: list[list[str]]) -> None:\n", + " self._grid = grid\n", + " \n", + " def get_grid_size(self) -> int:\n", + " return self._grid_size\n", + "\n", + " def set_grid_size(self, grid_size: int) -> None:\n", + " self._grid_size = grid_size\n", + "```\n", + "\n", + "### Defining the public interface\n", + "\n", + "In Python, we do things differently. Python does not have a notion of **private attributes**. Instead, attributes are named following conventions to indicate that they are non-public. In other words, users can still access them, but do so at their own risk. Such access is discouraged.\n", + "\n", + "For example, to mark our `Grid.grid` attribute as non-public, we can prefix it with an underscore:\n", + "\n", + "```python\n", + "class Grid:\n", + " def __init__(self, grid_size: int, char: str) -> None:\n", + " self._grid = create_grid(grid_size, char) # marked as non-public\n", + " self.grid_size = grid_size # public\n", + "```\n", + "\n", + "This describes a *contract*: as programmers, we are effectively telling other users \"you may access the `Grid.grid_size` attribute directly, but you should not access the `Grid._grid` attribute\". This is a convention, and it is up to the user to respect it. If they don't, they are responsible for any bugs that arise from this.\n", + "\n", + "How can users make changes to the grid then? We provide **public methods** for them to do so. Commonly, we will provide **getter** methods for them to get data, and **setter** methods for them to set data, also other utility methods to display data, or retrieve it in a different format.\n", + "\n", + "To summarise:\n", + "\n", + "- we separate interface from implementation\n", + "- by marking protected attributes as private (which means they should not be accessed directly by users)\n", + "- and providing public methods to access and modify them" + ] + }, + { + "cell_type": "markdown", + "id": "e0966dbc", + "metadata": {}, + "source": [ + "### `Grid`'s public interface\n", + "\n", + "Let's define the public interface of our `Grid` class. We will provide the following methods:\n", + "\n", + "- `Grid.get(x: int, y: int) -> str`: returns the `str` at the given coordinates\n", + "- `Grid.set(x: int, y: int, char: str) -> None`: sets the `str` at the given coordinates\n", + "- `Grid.is_occupied() -> bool`: returns `True` if the cell is occupied, `False` otherwise (empty)\n", + "- `Grid.display() -> None`: displays the formatted grid\n", + "\n", + "And we will also provide the following attributes:\n", + "\n", + "- `Grid.grid_size`: the square grid's size. Users may access this attribute directly, but should not modify it.\n", + "\n", + "#### Using the public interface\n", + "\n", + "Let's rewrite two functions, `is_target_hit()` and `targetting update()` to only use the public interface, without touching `Grid.grid`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439930fc", + "metadata": {}, + "outputs": [], + "source": [ + "from starting_code import EMPTY, HIT, MISS\n", + "\n", + "def is_target_hit(board: Grid, x: int, y: int) -> bool:\n", + " \"\"\"Check if the target hit a ship. Return True if it did, False otherwise.\"\"\"\n", + " return board.get(x, y) != EMPTY\n", + "\n", + "def targetting_update(board: Grid, hit_what: str, x: int, y: int) -> None:\n", + " \"\"\"Update the targetting board with the guess.\"\"\"\n", + " if hit_what == EMPTY:\n", + " board.set(x, y, MISS)\n", + " else:\n", + " board.set(x, y, HIT)" + ] + }, + { + "cell_type": "markdown", + "id": "b18d814c", + "metadata": {}, + "source": [ + "Now we have eliminated any assumption about the grid's internal representation and format. We use the **interface**--the public `get()` and `set()` methods--to update the board data, which lets us use the board without having to know anything about its **implementation**--the `_grid` attribute. This is a good design, because it allows us to change the implementation without breaking the code that uses it.\n", + "\n", + "For example, our `Grid` might look like this now:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3d800ca", + "metadata": {}, + "outputs": [], + "source": [ + "# Docstrings and other methods of the interface are omitted for brevity.\n", + "from starting_code import HORIZONTAL, VERTICAL, EMPTY\n", + "\n", + "class Grid:\n", + " \"\"\"Represents a grid for a game board.\n", + " This implementation uses a 2D list of lists to represent the grid.\n", + " \"\"\"\n", + " def __init__(self, grid_size: int, char: str = EMPTY):\n", + " self._grid = create_grid(grid_size, char)\n", + " self.grid_size = grid_size\n", + "\n", + " def get(self, x: int, y: int) -> str:\n", + " return self.grid[x][y]\n", + " \n", + " def set(self, x: int, y: int, char: str) -> None:\n", + " self.grid[x][y] = char\n", + "\n", + " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", + " if orientation == HORIZONTAL:\n", + " for i in range(size):\n", + " if self.get(row, col + i) != EMPTY:\n", + " return False\n", + " elif orientation == VERTICAL:\n", + " for i in range(size):\n", + " if self.get(row + i, col) != EMPTY:\n", + " return False\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "id": "cdeb6521", + "metadata": {}, + "source": [ + "### Refactoring the implementation painlessly\n", + "\n", + "If I decide to use a different implementation of the grid--such as a 1D list--I can do so without breaking the code that uses it. I just need to change the `get()` and `set()` methods to use the new implementation, and everything else will work as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41c5c40e", + "metadata": {}, + "outputs": [], + "source": [ + "# Docstrings and other methods of the interface are omitted for brevity.\n", + "from starting_code import HORIZONTAL, VERTICAL, EMPTY\n", + "\n", + "class Grid:\n", + " \"\"\"Represents a grid for a game board.\n", + " This implementation uses a 1D list to represent the grid.\n", + " coordinates (x, y) are translated into an index using the formula `x * grid_size + y`.\n", + " \"\"\"\n", + " def __init__(self, grid_size: int, char: str = EMPTY):\n", + " self._grid = [char] * (grid_size * grid_size)\n", + " self.grid_size = grid_size\n", + "\n", + " def get(self, x: int, y: int) -> str:\n", + " return self.grid[x * self.grid_size + y]\n", + " \n", + " def set(self, x: int, y: int, char: str) -> None:\n", + " self.grid[x * self.grid_size + y] = char\n", + "\n", + " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", + " if orientation == HORIZONTAL:\n", + " for i in range(size):\n", + " if self.get(row, col + i) != EMPTY:\n", + " return False\n", + " elif orientation == VERTICAL:\n", + " for i in range(size):\n", + " if self.get(row + i, col) != EMPTY:\n", + " return False\n", + " return True\n", + " \n", + "# Any code using `Grid` will continue to work without requiring any change,\n", + "# as long as they abide by the contract and did not access the grid directly,\n", + "# and provided this implementation is free of bugs." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb new file mode 100644 index 0000000..c615264 --- /dev/null +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -0,0 +1,322 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1189fcd9", + "metadata": {}, + "source": [ + "## 1-minute introduction to Jupyter ##\n", + "\n", + "A Jupyter notebook consists of cells. Each cell contains either text or code.\n", + "\n", + "A text cell will not have any text to the left of the cell. A code cell has `In [ ]:` to the left of the cell.\n", + "\n", + "If the cell contains code, you can edit it. Press Enter to edit the selected cell. While editing the code, press Enter to create a new line, or Shift+Enter to run the code. If you are not editing the code, select a cell and press Ctrl+Enter to run the code." + ] + }, + { + "cell_type": "markdown", + "id": "8d87d976", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e933773d", + "metadata": {}, + "source": [ + "# Object-Oriented Programming\n", + "\n", + "This lesson continues from the previous chapter on Encapsulation. In that lesson, we created a `Grid` class with a public interface, including `get()` and `set()` methods. The public **interface** hides the **implementation** details of the class, allowing the implementation code to be refactored without affecting other code that relies on the public interface.\n", + "\n", + "Yet one problem remains. We actually have two kinds of boards: a targetting board that tracks hits and misses (using `X` and `O`), and a playing board that tracks ship positions (using ship letters). Mixing up the two kinds of boards could be catastrophic.\n", + "\n", + "The player and enemy tracking and playing boards now all use `Grid`, and the use of methods like `Grid.display()` prevents us from passing the wrong board. However, functions like `display_overlay()` which takes two different boards may still be called with the wrong board.\n", + "\n", + "In this lesson, we will create `TrackingBoard` and `PlayingBoard` classes. These two classes bundle additional data and methods specific to their purpose, yet can still be used wherever `Grid` is used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c901d91e", + "metadata": {}, + "outputs": [], + "source": [ + "# Run this cell to make the Grid class available for subsequent cells.\n", + "from starting_code import HORIZONTAL, VERTICAL, EMPTY, HIT, MISS, create_grid\n", + "\n", + "class Grid:\n", + " \"\"\"Represents a grid in Battleships.\"\"\"\n", + " def __init__(self, grid_size, char: str):\n", + " self.grid_size = grid_size\n", + " self._grid = create_grid(grid_size, char)\n", + "\n", + " def get(self, x: int, y: int) -> str:\n", + " \"\"\"Get the character at the specified coordinates.\"\"\"\n", + " return self._grid[y][x]\n", + " \n", + " def set(self, x: int, y: int, char: str) -> None:\n", + " \"\"\"Set the character at the specified coordinates.\"\"\"\n", + " self._grid[y][x] = char\n", + "\n", + " def display(self) -> None:\n", + " \"\"\"Display the current state of the board.\"\"\"\n", + " # Column label row\n", + " print(\" \" + \" \".join(str(i) for i in range(self.grid_size)))\n", + " # Row label followed by the row contents\n", + " for i, row in enumerate(self._grid):\n", + " print(f\"{i} \" + \" \".join(row))\n" + ] + }, + { + "cell_type": "markdown", + "id": "53bdcba3", + "metadata": {}, + "source": [ + "## `TrackingBoard` and `PlayingBoard` classes\n", + "\n", + "Looking at our starting code again, we can see `TrackingBoard` which tracks hits and misses will require the following attributes and methods:\n", + "\n", + "- `hits`: int\n", + " Number of hits scored\n", + "- `targetting_update(hit_what: str, x: int, y: int)`\n", + " Update the targetting board with a hit or miss\n", + "\n", + "The `PlayingBoard` class will require the following attributes and methods:\n", + "\n", + "- `ship_cells`: dict[str, int]\n", + " Number of cells remaining for each ship on the board\n", + "- `sunk_ships`: list[str]\n", + " List of sunk ships\n", + "- `is_target_hit(x: int, y: int) -> bool`\n", + " Check if the opponent scored a hit on this board\n", + "- `playing_update(x: int, y: int)`\n", + " Update the playing board with opponent's guesses\n", + "\n", + "Despite needing a different set of attributes and methods, both classes will still need to be useable wherever `Grid` is used. This means we need to be able to call `display()` on both classes, and pass them to functions that take a `Grid` as a parameter.\n", + "\n", + "One way to do this is to copy all the methods from `Grid` into both classes. This is called **code duplication** and is a bad idea. If we need to change the implementation of `display()`, we would have to change it correctly in three places.\n", + "\n", + "Wouldn't it be nice if `TrackingBoard` and `PlayingBoard` could just use those methods from `Grid`, without having to copy them? This is where **inheritance** comes in." + ] + }, + { + "cell_type": "markdown", + "id": "221676a8", + "metadata": {}, + "source": [ + "## Inheritance\n", + "\n", + "**Inheritance** is a property whereby a **child class** (or **subclass**) is able to use the **public** methods and attributes of a **parent class** (or **superclass**). In our case, `TrackingBoard` and `PlayingBoard` will inherit from `Grid`. This means they will have access to all the methods and attributes of `Grid`, including `display()`, `get()`, and `set()`.\n", + "\n", + "(**Note:** the inheritance relationship is one-way; `Grid` is unable to use the methods and attributes of `TrackingBoard` or `PlayingBoard`.)\n", + "\n", + "In Python, we apply inheritance using parentheses in the class definition. For example, to create a `TrackingBoard` class that inherits from `Grid`, we would write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70452932", + "metadata": {}, + "outputs": [], + "source": [ + "class TrackingBoard(Grid):\n", + " \"\"\"Tracks hits and misses on a grid.\"\"\"\n", + "\n", + "board = TrackingBoard(10, EMPTY)\n", + "board.display()" + ] + }, + { + "cell_type": "markdown", + "id": "077a5d98", + "metadata": {}, + "source": [ + "Notice that even without implementing any new methods, `TrackingBoard` already has access to all the methods and attributes of `Grid`. This is because `TrackingBoard` is a subclass of `Grid`, and therefore inherits all its methods and attributes.\n", + "\n", + "Subclasses are also recognised as instances of their parent class. This means that if we have a function that takes a `Grid` as a parameter, we can also pass in a `TrackingBoard`. For exact type matches, use `type(variable) == Class` instead.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a39b0d3d", + "metadata": {}, + "outputs": [], + "source": [ + "print(isinstance(board, TrackingBoard)) # Should print True\n", + "print(isinstance(board, Grid)) # Should print True\n", + "print(type(board) == TrackingBoard) # Should print True\n", + "print(type(board) == Grid) # Should print False" + ] + }, + { + "cell_type": "markdown", + "id": "8b132afd", + "metadata": {}, + "source": [ + "\n", + "To implement additional methods and attributes, we simply define them in the `TrackingBoard` class. For example, to implement the `targetting_update()` method, we would write:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eea8540c", + "metadata": {}, + "outputs": [], + "source": [ + "class TrackingBoard(Grid):\n", + " \"\"\"Tracks hits and misses on a grid.\"\"\"\n", + " \n", + " def update(self, hit_what: str, x: int, y: int) -> None:\n", + " \"\"\"Update the grid with a hit or miss.\"\"\"\n", + " if hit_what == EMPTY:\n", + " self.set(x, y, MISS)\n", + " else:\n", + " self.set(x, y, HIT)\n", + "\n", + "board = TrackingBoard(10, EMPTY)\n", + "board.update('B', 2, 3)\n", + "board.display()" + ] + }, + { + "cell_type": "markdown", + "id": "2bd7b380", + "metadata": {}, + "source": [ + "### Method overriding and extending\n", + "\n", + "We also need to implement the `__init__()` method. `Grid.__init__()` only sets the `_grid` and `grid_size` attributes, but we also need to set `hits = 0` for `TrackingBoard`. We could do:\n", + "\n", + "```python\n", + "class TrackingBoard(Grid):\n", + " def __init__(self, grid_size: int, char: str):\n", + " self._grid = create_grid(grid_size, char)\n", + " self.grid_size = grid_size\n", + " self.hits = 0\n", + "```\n", + "\n", + "This would be code duplication, which we are trying to avoid, so as to make refactoring easier. What if we could first call `Grid.__init__()` to set the `_grid` and `grid_size` attributes, *and then* set `hits = 0`? This is where the `super()` function comes in.\n", + "\n", + "The `super()` function returns a temporary object of the superclass that allows us to call its methods. This means we can call `Grid.__init__()` from within `TrackingBoard.__init__()` to set the `_grid` and `grid_size` attributes, and then set `hits = 0`. The code would look like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "069addea", + "metadata": {}, + "outputs": [], + "source": [ + "class TrackingBoard(Grid):\n", + " \"\"\"Tracks hits and misses on a grid.\"\"\"\n", + " def __init__(self, grid_size: int, char: str):\n", + " \"\"\"Invoke the parent constructor to set grid_size and _grid.\"\"\"\n", + " super().__init__(grid_size, char)\n", + " # Then continue to initialise additional attributes.\n", + " self.hits = 0\n", + "\n", + " def update(self, hit_what: str, x: int, y: int) -> None:\n", + " \"\"\"Update the grid with a hit or miss.\"\"\"\n", + " if hit_what == EMPTY:\n", + " self.set(x, y, MISS)\n", + " else:\n", + " self.set(x, y, HIT)\n", + "\n", + "board = TrackingBoard(10, EMPTY)\n", + "print(\"grid_size:\", board.grid_size)\n", + "print(\"hits:\", board.hits)" + ] + }, + { + "cell_type": "markdown", + "id": "7c2fcb72", + "metadata": {}, + "source": [ + "Note that by defining `__init__()` in `TrackingBoard`, we are **overriding** the `__init__()` method of `Grid`. This means that when we create a `TrackingBoard` object (with `TrackingBoard(10, EMPTY)`), it will use the `__init__()` method of `TrackingBoard`, not the one from `Grid`. If we did not call the `__init__()` method of `Grid` using `super()`, it would not be invoked.\n", + "\n", + "Using `super().__init__()` allows us to call the `__init__()` method of the superclass, which sets the `_grid` and `grid_size` attributes. We can then set any additional attributes we need in the child class. This pattern is called **method extension**. It allows us to extend the functionality of a method in a child class while still using the implementation from the parent class." + ] + }, + { + "cell_type": "markdown", + "id": "2dd5bfbe", + "metadata": {}, + "source": [ + "## Exercise 1\n", + "\n", + "Implement the `PlayingBoard` class. It should inherit from `Grid`, and implement the following methods:\n", + "- `__init__(self, grid_size: int, char: str)`\n", + " Call `Grid.__init__()` to set the `_grid` and `grid_size` attributes, and set `ship_cells = {}` and `sunk_ships = []`.\n", + "- `is_target_hit(self, x: int, y: int) -> bool`\n", + " Check if the opponent scored a hit on this board. If the cell is not empty, return `True`. Otherwise, return `False`.\n", + "- `update(self, x: int, y: int)` (renamed from `playing_update`)\n", + " Update the playing board with opponent's guesses. If the cell is not empty, remove the ship from `ship_cells` and add it to `sunk_ships`. Otherwise, do nothing.\n", + "\n", + "You may edit the starting code to implement the above. Remember to obey the public interface of the `Grid` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de8d1f4f", + "metadata": {}, + "outputs": [], + "source": [ + "# Write your code for Exercise 1 here.\n" + ] + }, + { + "cell_type": "markdown", + "id": "17f9f5d8", + "metadata": {}, + "source": [ + "## Exercise 2\n", + "\n", + "Extend the `display()` method of the `Grid` parent class.\n", + "\n", + "- `TrackingBoard.display()` should display `Hits: ` at the top of the board, where `` is the number of hits scored\n", + "- `PlayingBoard.display()` should display `Ships sunk: <...>` at the bottom of the board, where `<...>` are letters of ships sunk.\n", + "\n", + "You can use `super().display()` to call the `display()` method of the parent class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4d1b340", + "metadata": {}, + "outputs": [], + "source": [ + "# Write your code for Exercise 2 here." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb new file mode 100644 index 0000000..5e49035 --- /dev/null +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1189fcd9", + "metadata": {}, + "source": [ + "## 1-minute introduction to Jupyter ##\n", + "\n", + "A Jupyter notebook consists of cells. Each cell contains either text or code.\n", + "\n", + "A text cell will not have any text to the left of the cell. A code cell has `In [ ]:` to the left of the cell.\n", + "\n", + "If the cell contains code, you can edit it. Press Enter to edit the selected cell. While editing the code, press Enter to create a new line, or Shift+Enter to run the code. If you are not editing the code, select a cell and press Ctrl+Enter to run the code." + ] + }, + { + "cell_type": "markdown", + "id": "8d87d976", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "e933773d", + "metadata": {}, + "source": [ + "# Object-Oriented Programming\n", + "\n", + "This lesson continues from the previous chapter on Inheritance. In that lesson, we created `TrackingBoard` and `PlayingBoard` classes that inherit from `Grid`, allowing them to be used wherever `Grid` is required, yet also adding additional functionality. This avoids code duplication and encourages code reuse.\n", + "\n", + "Now we turn our attention to the player. In the starting code, our game loop looked like this:\n", + "\n", + "```python\n", + " while not is_gameover(turns, player_hits, enemy_hits, total_ship_cells):\n", + " print(\"\\nTurns left:\", turns)\n", + " print(\"\\nPlayer's turn\")\n", + " display_board(player_targetting)\n", + " x, y = prompt_valid_guess(player_targetting)\n", + "\n", + " hit_char = enemy_board[x][y]\n", + " targetting_update(player_targetting, hit_char, x, y)\n", + " if is_target_hit(enemy_board, x, y):\n", + " player_hits += 1\n", + " enemy_ship_cells[hit_char] -= 1\n", + " if enemy_ship_cells[hit_char] == 0:\n", + " print(\"You sunk the\", hit_char + \"!\")\n", + " player_sunk_ships.append(hit_char)\n", + " print(\"Ships sunk:\", \" \".join(player_sunk_ships) or \"None\")\n", + "\n", + " print(\"\\nEnemy's turn\")\n", + " x, y = get_enemy_guess(player_board)\n", + "\n", + " hit_char = player_board[x][y]\n", + " targetting_update(enemy_targetting, hit_char, x, y)\n", + " if is_target_hit(player_board, x, y):\n", + " enemy_hits += 1\n", + " player_ship_cells[hit_char] -= 1\n", + " if player_ship_cells[hit_char] == 0:\n", + " print(\"Enemy sunk the\", hit_char + \"!\")\n", + " enemy_sunk_ships.append(hit_char)\n", + " display_overlay(enemy_targetting, player_board)\n", + " print(\"Ships sunk:\", \" \".join(enemy_sunk_ships) or \"None\")\n", + " turns -= 1\n", + "```\n", + "\n", + "This code is tricky to abstract, because the player and enemy must be handled differently. The player must be prompted for input, while the enemy's guess is generated randomly. The player also has a different board to display than the enemy. We can abstract this code by creating a `Player` class that handles the player's input and a `Computer` class that handles the computer's input. This allows us to create a single game loop that works for both players and computers.\n", + "\n", + "What if we could rewrite the code like this?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c99fea7e", + "metadata": {}, + "outputs": [], + "source": [ + "# You may wish to copy your TargettingBoard and PlayingBoard class from the previous exercise here\n", + "from starting_code import GRID_SIZE, EMPTY, HIT, MISS\n", + "\n", + "class Commander:\n", + " \"\"\"Superclass for Player and Computer.\"\"\"\n", + " def __init__(self, name):\n", + " self.name = name\n", + " self.targetting = TargettingBoard(GRID_SIZE, EMPTY)\n", + " self.playing = PlayingBoard(GRID_SIZE, EMPTY)\n", + "\n", + " def guess(self) -> tuple[int, int]:\n", + " \"\"\"Returns a tuple of integers representing the coordinates of the guess.\"\"\"\n", + " raise NotImplementedError(\"Subclasses must implement this method.\")\n", + "\n", + "class Player(Commander):\n", + " \"\"\"We will implement guess() later\"\"\"\n", + "\n", + "class Computer:\n", + " \"\"\"We will implement guess() later\"\"\"\n", + " \n", + "\n", + "def execute_turn(attacker: Commander, defender: Commander) -> None:\n", + " \"\"\"Executes a turn for each commander.\"\"\"\n", + " # Attacker's turn\n", + " print(f\"\\n{attacker.name}'s turn\")\n", + " attacker.targetting.display()\n", + " x, y = attacker.guess()\n", + "\n", + " hit_char = defender.playing.get(x, y)\n", + " attacker.targetting.update(x, y, hit_char) # also updates attacker.targetting.hits\n", + " defender.playing.update(x, y) # updates ship_cells and sunk_ships\n", + " if defender.is_sunk(hit_char): # assuming the existence of this method; it can be implemented if not existent\n", + " print(f\"{attacker} sunk the\", hit_char + \"!\")\n", + " # Only the player needs to see the overlay\n", + " if isinstance(attacker, Player):\n", + " display_overlay(attacker.targetting, defender.board)\n", + "\n", + "def run_game(turns: int, ships: dict[str, int]) -> None:\n", + " total_ship_cells = sum(ships.values())\n", + " player = Player(\"You\")\n", + " enemy = Computer(\"Enemy\")\n", + " # Place ships on the boards\n", + " for board in [player.playing, enemy.playing]:\n", + " for name, size in ships.items():\n", + " board.place_ship(name, size)\n", + " # ship_cells are updated within the place_ship() method\n", + "\n", + " while not is_gameover(turns, player, enemy, total_ship_cells):\n", + " print(\"\\nTurns left:\", turns)\n", + " execute_turn(player, enemy)\n", + " execute_turn(enemy, player)\n", + " turns -= 1\n", + "\n", + " # Game is over\n", + " if is_won(player, total_ship_cells):\n", + " print(\"Congratulations! You sank all the enemy ships!\")\n", + " display_overlay(player_targetting, enemy_board)\n", + " elif is_won(enemy, total_ship_cells):\n", + " print(\"Game over! The enemy sank all your ships!\")\n", + " display_overlay(enemy_targetting, player_board)\n", + " else:\n", + " print(\"Game over! You ran out of turns!\")\n", + " display_overlay(player_targetting, enemy_board)\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b1fde2b", + "metadata": {}, + "source": [ + "## Polymorphism\n", + "\n", + "This pattern enables us to compress the game loop significantly; we avoiding writing lots of if-else statements to handle the player and computer differently, and we also avoid code duplication for the player and computer. The tradeoff is that the player and computer must **share a common interface**. For `execute_turn()` to work, both `Player` and `Computer` must have a public `name` attribute and a public `guess()` method.\n", + "\n", + "`Player.guess()` will use the `input()` function to prompt the player for input, validating it and converting it into a tuple, while `Computer.guess()` will generate a random valid guess. Although both classes obey the same interface, they are implemented differently. This is the essence of the principle of **polymorphism**: different classes can be used interchangeably as long as they share a common interface." + ] + }, + { + "cell_type": "markdown", + "id": "35231adc", + "metadata": {}, + "source": [ + "## Exercise 1\n", + "\n", + "Implement the `Player` and `Computer` classes. The `Player` class should have a `name` attribute and a `guess()` method that prompts the player for input. The `Computer` class should have a `name` attribute and a `guess()` method that generates a random valid guess.\n", + "\n", + "You may use the starting code, refactoring it to achieve the above requirements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a2e9ec8", + "metadata": {}, + "outputs": [], + "source": [ + "# Write your code below" + ] + }, + { + "cell_type": "markdown", + "id": "7e03115a", + "metadata": {}, + "source": [ + "## Exercise 2\n", + "\n", + "In a separate file, `battleships.py`, rewrite the starting code, applying the principles of encapsulation, inheritance, and polymorphism.\n", + "\n", + "Your code should have the following classes:\n", + "\n", + "- `Grid`: The base class for the game board.\n", + "- `TrackingBoard`: A subclass of `Grid` that tracks hits and misses.\n", + "- `PlayingBoard`: A subclass of `Grid` that tracks ship positions.\n", + "- `Commander`: The base class for the player and computer.\n", + "- `Player`: A class that represents the player.\n", + "- `Computer`: A class that represents the computer.\n", + "\n", + "Introduce any necessary helper functions you require, and remove any unnecessary functions." + ] + }, + { + "cell_type": "markdown", + "id": "b987f527", + "metadata": {}, + "source": [ + "## Exercise 3 (optional)\n", + "\n", + "In a separate `game.py` file, encapsulate the game loop in a `BattleshipsGame` class. The `BattleshipsGame` class should have the following public interface:\n", + "\n", + "- `__init__(self, player_name: str)`: Initialises the game\n", + "- `play(self)`: Starts the game loop\n", + "- `is_gameover(self)`: Checks if the game is over\n", + "\n", + "(You may introduce any other helper methods, but they should be private.)\n", + "\n", + "The game should be playable with the following code example:\n", + "\n", + "```python\n", + "from battleships import BattleshipsGame\n", + "\n", + "game = BattleshipsGame(\"You\")\n", + "game.play()\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Object-Oriented Programming/starting_code.py b/Object-Oriented Programming/starting_code.py new file mode 100644 index 0000000..ce2f26c --- /dev/null +++ b/Object-Oriented Programming/starting_code.py @@ -0,0 +1,245 @@ +import random + +GRID_SIZE = 10 +EMPTY = "~" +HIT = "X" +MISS = "O" + +HORIZONTAL = "horizontal" +VERTICAL = "vertical" + + +def create_grid(n: int, char: str) -> list[list[str]]: + """Create a grid of size n x n filled with char, and return it.""" + grid = [] + for _ in range(n): + grid.append([char] * n) + return grid + +def is_unoccupied(board: list[list[str]], row: int, col: int, size: int, orientation: str) -> bool: + """Check if the specified area on the board is unoccupied. + + Return True if unoccupied, False otherwise. + """ + if orientation == HORIZONTAL: + for i in range(size): + if board[row][col + i] != EMPTY: + return False + elif orientation == VERTICAL: + for i in range(size): + if board[row + i][col] != EMPTY: + return False + return True + +def place_ship_horizontally(board: list[list[str]], ship_name: str, size: int, row: int, col: int) -> None: + """Place a ship horizontally on the board at the specified location. + + Return True if successful, False if not. + + Carry out validation first to check that the ship fits in the specified location and that the space is empty. + """ + for i in range(size): + board[row][col + i] = ship_name + +def place_ship_vertically(board: list[list[str]], ship_name: str, size: int, row: int, col: int) -> None: + """Place a ship vertically on the board at the specified location. + + Return True if successful, False if not. + + Carry out validation first to check that the ship fits in the specified location and that the space is empty. + """ + for i in range(size): + board[row + i][col] = ship_name + +def place_ship(board: list[list[str]], ship_name: str, size: int) -> None: + """Place a ship of size size on the board at a random location.""" + orientation = random.choice([HORIZONTAL, VERTICAL]) + placed = False + while not placed: + row = random.randint(0, GRID_SIZE - 1) + col = random.randint(0, GRID_SIZE - 1) + if orientation == HORIZONTAL and col + size <= GRID_SIZE: + if is_unoccupied(board, row, col, size, HORIZONTAL): + place_ship_horizontally(board, ship_name, size, row, col) + placed = True + elif orientation == VERTICAL and row + size <= GRID_SIZE: + if is_unoccupied(board, row, col, size, VERTICAL): + place_ship_vertically(board, ship_name, size, row, col) + placed = True + +def display_board(board: list[list[str]]) -> None: + """Display the current state of the board.""" + # Column label row + print(" " + " ".join(str(i) for i in range(GRID_SIZE))) + # Row label followed by the row contents + for i, row in enumerate(board): + print(f"{i} " + " ".join(row)) + +def is_valid_guess(board: list[list[str]], text: str) -> bool: + """Check if the guess is valid. Return True if valid, False otherwise. + + A guess is expected in the form "row,col", where row and col are integers between 0 and GRID_SIZE - 1. + The guess is valid if the coordinates are within the bounds of the board and the cell has not been + guessed before (not HIT or MISS). + """ + if text.count(",") != 1: + return False + first, second = text.split(",") + if not first.isdigit() or not second.isdigit(): + return False + x, y = int(first), int(second) + if not ((0 <= x < GRID_SIZE) and (0 <= y < GRID_SIZE)): + return False + if board[x][y] in (HIT, MISS): + return False + return True + +def guess_to_coordinates(guess: str) -> tuple[int, int]: + """Convert a guess string to coordinates. + + Return the row and column as a tuple of integers. + """ + first, second = guess.split(",") + x, y = int(first), int(second) + return x, y + +def prompt_valid_guess(board: list[list[str]]) -> tuple[int, int]: + """Prompt the user for a valid guess. + + If the guess is invalid, keep prompting until a valid guess is entered. + + Return the row and column of the guess, as a tuple. + """ + guess = input("Enter your guess (row,col): ") + while not is_valid_guess(board, guess): + guess = input("Invalid guess. Enter your guess (row,col): ") + return guess_to_coordinates(guess) + +def get_enemy_guess(board: list[list[str]]) -> tuple[int, int]: + """Generate a random guess for the enemy. + + Return the row and column of the guess, as a tuple. + """ + x = random.randint(0, GRID_SIZE - 1) + y = random.randint(0, GRID_SIZE - 1) + while board[x][y] in (HIT, MISS): + x = random.randint(0, GRID_SIZE - 1) + y = random.randint(0, GRID_SIZE - 1) + return x, y + +def is_target_hit(board: list[list[str]], x: int, y: int) -> bool: + """Check if the target hit a ship. Return True if it did, False otherwise.""" + return board[x][y] != EMPTY + +def targetting_update(board: list[list[str]], hit_what: str, x: int, y: int) -> None: + """Update the targetting board with the guess.""" + if hit_what == EMPTY: + board[x][y] = MISS + else: + board[x][y] = HIT + +def player_update(board: list[list[str]], x: int, y: int) -> None: + """Update the player board with the guess.""" + if board[x][y] == EMPTY: + board[x][y] = MISS + else: + board[x][y] = HIT + +def is_won(hits: int, total_ship_cells: int) -> bool: + """Check if the player has won. Return True if they have, False otherwise.""" + return hits == total_ship_cells + +def is_gameover(turns: int, player_hits: int, enemy_hits: int, total_ship_cells: int) -> bool: + """Check if the game is over. Return True if it is, False otherwise.""" + if is_won(player_hits, total_ship_cells) or is_won(enemy_hits, total_ship_cells): + return True + if turns <= 0: + return True + return False + +def display_overlay(targetting: list[list[str]], playing: list[list[str]]) -> None: + """Display the targetting overlaid on the ship board.""" + # Column label row + print(" " + " ".join(str(i) for i in range(GRID_SIZE))) + # Row label followed by the row contents + for i in range(GRID_SIZE): + print(f"{i} ", end="") + for j in range(GRID_SIZE): + if targetting[i][j] == HIT: + print(HIT, end=" ") + elif targetting[i][j] == MISS: + print(MISS, end=" ") + else: + print(playing[i][j], end=" ") + print() # end the row + +def run_game(turns: int, ships: dict[str, int]) -> None: + # Variables for tracking game state + player_hits = 0 + enemy_hits = 0 + total_ship_cells = sum(ships.values()) + + # Data structures for tracking game data + # For tracking player ships and damage + player_board = create_grid(10, " ") + player_ship_cells = {} # To track each ship’s remaining cells + player_targetting = create_grid(10, " ") # For tracking player guesses and hits + player_sunk_ships = [] + + # Similarly for the enemy + enemy_board = create_grid(10, " ") + enemy_ship_cells = {} + enemy_targetting = create_grid(10, " ") + enemy_sunk_ships = [] + + # Place ships on the boards + for name, size in ships.items(): + place_ship(player_board, name, size) + player_ship_cells[name] = size + place_ship(enemy_board, name, size) + enemy_ship_cells[name] = size + + while not is_gameover(turns, player_hits, enemy_hits, total_ship_cells): + print("\nTurns left:", turns) + print("\nPlayer's turn") + display_board(player_targetting) + x, y = prompt_valid_guess(player_targetting) + + hit_char = enemy_board[x][y] + targetting_update(player_targetting, hit_char, x, y) + if is_target_hit(enemy_board, x, y): + player_hits += 1 + enemy_ship_cells[hit_char] -= 1 + if enemy_ship_cells[hit_char] == 0: + print("You sunk the", hit_char + "!") + player_sunk_ships.append(hit_char) + print("Ships sunk:", " ".join(player_sunk_ships) or "None") + + print("\nEnemy's turn") + x, y = get_enemy_guess(player_board) + + hit_char = player_board[x][y] + targetting_update(enemy_targetting, hit_char, x, y) + if is_target_hit(player_board, x, y): + enemy_hits += 1 + player_ship_cells[hit_char] -= 1 + if player_ship_cells[hit_char] == 0: + print("Enemy sunk the", hit_char + "!") + enemy_sunk_ships.append(hit_char) + display_overlay(enemy_targetting, player_board) + print("Ships sunk:", " ".join(enemy_sunk_ships) or "None") + turns -= 1 + + # Game is over + if player_hits == total_ship_cells: + print("Congratulations! You sank all the enemy ships!") + display_overlay(player_targetting, enemy_board) + elif enemy_hits == total_ship_cells: + print("Game over! The enemy sank all your ships!") + display_overlay(enemy_targetting, player_board) + else: + print("Game over! You ran out of turns!") + display_overlay(player_targetting, enemy_board) + +if __name__ == "__main__": + run_game(turns=30, ships={"B": 4, "C": 3, "D": 2}) diff --git a/README.md b/README.md index 4f8d204..6e5c396 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,14 @@ The following topics are covered: - composable functions - composing and decomposing functions - using constants + +### Object-Oriented Programming + +01 Encapsulation +- bundling data +- bundling methods +- separating interface and implementation +- the public interface + +02 Inheritance +- \ No newline at end of file From ed3f54bf2d3973b29ee111855bed01a1f26c1260 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 17:11:56 +0800 Subject: [PATCH 06/25] Update 01 Encapsulation.ipynb --- Object-Oriented Programming/01 Encapsulation.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 13240d7..4225abb 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -397,7 +397,7 @@ "\n", "Notice this relies on `self.grid` being a list of lists. This seems fine and natural to you at the moment, because you can't imagine other ways of implementing a 2D grid; surely it has to be a list of lists?!\n", "\n", - "But as you improve in skill, or as you add more functionality to the game such that a list of list of `str`s is no longer sufficient, we'll need to change the grid format. But doing this will require carefully inspecting the code, checking every place where we treated `Board.grid` as a list of lists, and changing it to the new format. This is error-prone and tedious.\n", + "But as you improve in skill, or as you add more functionality to the game such that a list of list of `str`s is no longer sufficient, we'll need to change the grid format. But doing this will require carefully inspecting the code, checking every place where we treated `Board.grid` as a list of lists, and changing it to the new format. **This is error-prone and tedious.**\n", "\n", "How would we design the interface of the `Grid` class to improve this? In classic OOP, we treat most attributes as **private**. This means that they are not directly accessible from outside the class. Instead, we provide **getter** and **setter** methods to access and modify them. In programming languages where this is enforced, such as Java, our `Grid` class would look like this:\n", "\n", From 79b08bb935277ba7ad274115e594506df35a15ca Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 17:13:40 +0800 Subject: [PATCH 07/25] Update 01 Encapsulation.ipynb --- Object-Oriented Programming/01 Encapsulation.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 4225abb..52b0bbd 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -439,9 +439,9 @@ "\n", "To summarise:\n", "\n", - "- we separate interface from implementation\n", - "- by marking protected attributes as private (which means they should not be accessed directly by users)\n", - "- and providing public methods to access and modify them" + "- we **separate interface from implementation**\n", + "- by marking **protected attributes as private** (which means they should not be accessed directly by users)\n", + "- and providing **public methods** to access and modify them" ] }, { From 19e8443259a324b0cd826dd35c4224c65aa7431e Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 17:16:04 +0800 Subject: [PATCH 08/25] Update 01 Encapsulation.ipynb --- Object-Oriented Programming/01 Encapsulation.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 52b0bbd..160c313 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -451,14 +451,14 @@ "source": [ "### `Grid`'s public interface\n", "\n", - "Let's define the public interface of our `Grid` class. We will provide the following methods:\n", + "Let's define the public interface of our `Grid` class. We will provide the following public methods:\n", "\n", "- `Grid.get(x: int, y: int) -> str`: returns the `str` at the given coordinates\n", "- `Grid.set(x: int, y: int, char: str) -> None`: sets the `str` at the given coordinates\n", "- `Grid.is_occupied() -> bool`: returns `True` if the cell is occupied, `False` otherwise (empty)\n", "- `Grid.display() -> None`: displays the formatted grid\n", "\n", - "And we will also provide the following attributes:\n", + "And we will also provide the following public attributes:\n", "\n", "- `Grid.grid_size`: the square grid's size. Users may access this attribute directly, but should not modify it.\n", "\n", @@ -560,7 +560,7 @@ " coordinates (x, y) are translated into an index using the formula `x * grid_size + y`.\n", " \"\"\"\n", " def __init__(self, grid_size: int, char: str = EMPTY):\n", - " self._grid = [char] * (grid_size * grid_size)\n", + " self._grid = [char] * (grid_size * grid_size) ## Use a 1D list to represent 2D grid\n", " self.grid_size = grid_size\n", "\n", " def get(self, x: int, y: int) -> str:\n", From 4fb8465b638cb655e0eba50d3414ee877055d8d5 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 09:19:52 +0000 Subject: [PATCH 09/25] add interface-implementation exercise --- .../01 Encapsulation.ipynb | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 160c313..d88c030 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -584,6 +584,31 @@ "# as long as they abide by the contract and did not access the grid directly,\n", "# and provided this implementation is free of bugs." ] + }, + { + "cell_type": "markdown", + "id": "33afae56", + "metadata": {}, + "source": [ + "### Exercise: separating interface from implementation\n", + "\n", + "Practise applying what you just learnt. Copying your code from the previous exercise [\"Exercise: bundling data and methods\"](#exercise-bundling-data-and-methods): separate the interface from the implementation of the `Grid` class. You will need to:\n", + "- implement the `get()` method\n", + "- implement the `set()` method\n", + "- refactor the other methods to use the `get()` and `set()` methods, instead of accessing the `_grid` attribute directly\n", + "\n", + "Test your encapsulated `Grid` class. Does it work as expected?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a29c4706", + "metadata": {}, + "outputs": [], + "source": [ + "# Write your code here" + ] } ], "metadata": { From d9c3647c0407dfb65124b34a4bf69665fca3d6a1 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 11:23:53 +0000 Subject: [PATCH 10/25] Update 02 Inheritance --- Object-Oriented Programming/02 Inheritance.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index c615264..bacbe5c 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -33,7 +33,7 @@ "\n", "Yet one problem remains. We actually have two kinds of boards: a targetting board that tracks hits and misses (using `X` and `O`), and a playing board that tracks ship positions (using ship letters). Mixing up the two kinds of boards could be catastrophic.\n", "\n", - "The player and enemy tracking and playing boards now all use `Grid`, and the use of methods like `Grid.display()` prevents us from passing the wrong board. However, functions like `display_overlay()` which takes two different boards may still be called with the wrong board.\n", + "The player and enemy tracking and playing boards now all use `Grid`, and the use of methods like `Grid.display()` prevents us from passing the wrong board to the display method. However, functions like `display_overlay(targetting: Grid, playing: Grid)` which take two grids may still have their arguments accidentally swapped.\n", "\n", "In this lesson, we will create `TrackingBoard` and `PlayingBoard` classes. These two classes bundle additional data and methods specific to their purpose, yet can still be used wherever `Grid` is used." ] @@ -96,7 +96,7 @@ "- `playing_update(x: int, y: int)`\n", " Update the playing board with opponent's guesses\n", "\n", - "Despite needing a different set of attributes and methods, both classes will still need to be useable wherever `Grid` is used. This means we need to be able to call `display()` on both classes, and pass them to functions that take a `Grid` as a parameter.\n", + "Despite presenting a different set of attributes and methods, both classes will still need to be useable wherever `Grid` is used. This means we need to be able to call `get()`, `set()`, and `display()` on both classes, and pass them to functions that take a `Grid` as a parameter.\n", "\n", "One way to do this is to copy all the methods from `Grid` into both classes. This is called **code duplication** and is a bad idea. If we need to change the implementation of `display()`, we would have to change it correctly in three places.\n", "\n", @@ -138,7 +138,7 @@ "source": [ "Notice that even without implementing any new methods, `TrackingBoard` already has access to all the methods and attributes of `Grid`. This is because `TrackingBoard` is a subclass of `Grid`, and therefore inherits all its methods and attributes.\n", "\n", - "Subclasses are also recognised as instances of their parent class. This means that if we have a function that takes a `Grid` as a parameter, we can also pass in a `TrackingBoard`. For exact type matches, use `type(variable) == Class` instead.\n" + "Child classes are also recognised as instances of their parent class. This means that if we have a function that takes a `Grid` as a parameter, we can also pass in a `TrackingBoard`. For exact type matches, use `type(variable) == Class` instead.\n" ] }, { @@ -173,7 +173,7 @@ "class TrackingBoard(Grid):\n", " \"\"\"Tracks hits and misses on a grid.\"\"\"\n", " \n", - " def update(self, hit_what: str, x: int, y: int) -> None:\n", + " def update(self, hit_what: str, x: int, y: int) -> None: # renamed from targetting_update()\n", " \"\"\"Update the grid with a hit or miss.\"\"\"\n", " if hit_what == EMPTY:\n", " self.set(x, y, MISS)\n", @@ -202,7 +202,7 @@ " self.hits = 0\n", "```\n", "\n", - "This would be code duplication, which we are trying to avoid, so as to make refactoring easier. What if we could first call `Grid.__init__()` to set the `_grid` and `grid_size` attributes, *and then* set `hits = 0`? This is where the `super()` function comes in.\n", + "This would be code duplication, which we are trying to avoid, because it makes refactoring trickier. What if we could first call `Grid.__init__()` to set the `_grid` and `grid_size` attributes, *and then* set `hits = 0`? This is where the `super()` function comes in.\n", "\n", "The `super()` function returns a temporary object of the superclass that allows us to call its methods. This means we can call `Grid.__init__()` from within `TrackingBoard.__init__()` to set the `_grid` and `grid_size` attributes, and then set `hits = 0`. The code would look like this:" ] From 7b272f2fac5bc9821c0526bbad7e501cf629883b Mon Sep 17 00:00:00 2001 From: JS Ng Date: Mon, 14 Apr 2025 11:32:38 +0000 Subject: [PATCH 11/25] Update 03 Polymorphism --- .../03 Polymorphism.ipynb | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb index 5e49035..3a4a694 100644 --- a/Object-Oriented Programming/03 Polymorphism.ipynb +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -66,11 +66,25 @@ " turns -= 1\n", "```\n", "\n", - "This code is tricky to abstract, because the player and enemy must be handled differently. The player must be prompted for input, while the enemy's guess is generated randomly. The player also has a different board to display than the enemy. We can abstract this code by creating a `Player` class that handles the player's input and a `Computer` class that handles the computer's input. This allows us to create a single game loop that works for both players and computers.\n", + "This code is tricky to abstract, because the player and enemy must be handled differently. The player must be prompted for input, while the enemy's guess is generated randomly. The player also has a different board to display than the enemy.\n", + "\n", + "We can abstract this code by creating a `Player` class that handles the player's input and a `Computer` class that handles the computer's input. This allows us to create a single game loop that works for both players and computers.\n", "\n", "What if we could rewrite the code like this?\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea0929d2", + "metadata": {}, + "outputs": [], + "source": [ + "# Copy your TargettingBoard and PlayingBoard class from the previous exercise into this cell\n", + "# and run this cell so it is available for the example code cell below.\n", + "\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -78,11 +92,16 @@ "metadata": {}, "outputs": [], "source": [ - "# You may wish to copy your TargettingBoard and PlayingBoard class from the previous exercise here\n", "from starting_code import GRID_SIZE, EMPTY, HIT, MISS\n", "\n", "class Commander:\n", - " \"\"\"Superclass for Player and Computer.\"\"\"\n", + " \"\"\"Superclass for Player and Computer.\n", + "\n", + " Subclasses must implement the guess() method.\n", + " \n", + " Methods:\n", + " guess: Returns a tuple of integers representing the coordinates of the guess.\n", + " \"\"\"\n", " def __init__(self, name):\n", " self.name = name\n", " self.targetting = TargettingBoard(GRID_SIZE, EMPTY)\n", @@ -95,12 +114,26 @@ "class Player(Commander):\n", " \"\"\"We will implement guess() later\"\"\"\n", "\n", - "class Computer:\n", + "class Computer(Commander):\n", " \"\"\"We will implement guess() later\"\"\"\n", " \n", "\n", "def execute_turn(attacker: Commander, defender: Commander) -> None:\n", - " \"\"\"Executes a turn for each commander.\"\"\"\n", + " \"\"\"Executes a turn for each commander.\n", + " \n", + " A turn comprises:\n", + " 1. The attacker guesses a coordinate.\n", + " 2. The defender checks if the guess hits a ship.\n", + " 3. The attacker updates the targetting board with the result of the guess.\n", + " 4. The defender updates the playing board with the result of the guess.\n", + " 5. If the guess hits a ship, the defender checks if the ship is sunk.\n", + " 6. If the ship is sunk, the attacker is notified.\n", + " 7. The attacker and defender boards are displayed.\n", + "\n", + " Args:\n", + " attacker (Commander): The commander who is attacking.\n", + " defender (Commander): The commander who is defending.\n", + " \"\"\"\n", " # Attacker's turn\n", " print(f\"\\n{attacker.name}'s turn\")\n", " attacker.targetting.display()\n", @@ -125,8 +158,10 @@ " board.place_ship(name, size)\n", " # ship_cells are updated within the place_ship() method\n", "\n", - " while not is_gameover(turns, player, enemy, total_ship_cells):\n", + " while not is_gameover(turns: int, player: Player, enemy: Computer, total_ship_cells: int):\n", " print(\"\\nTurns left:\", turns)\n", + " # Use two execute_turn() function calls to execute a turn for each commander\n", + " # by swapping the attacker and defender\n", " execute_turn(player, enemy)\n", " execute_turn(enemy, player)\n", " turns -= 1\n", @@ -134,13 +169,13 @@ " # Game is over\n", " if is_won(player, total_ship_cells):\n", " print(\"Congratulations! You sank all the enemy ships!\")\n", - " display_overlay(player_targetting, enemy_board)\n", + " display_overlay(player, enemy)\n", " elif is_won(enemy, total_ship_cells):\n", " print(\"Game over! The enemy sank all your ships!\")\n", - " display_overlay(enemy_targetting, player_board)\n", + " display_overlay(enemy, player)\n", " else:\n", " print(\"Game over! You ran out of turns!\")\n", - " display_overlay(player_targetting, enemy_board)\n" + " display_overlay(player, enemy)\n" ] }, { @@ -150,7 +185,7 @@ "source": [ "## Polymorphism\n", "\n", - "This pattern enables us to compress the game loop significantly; we avoiding writing lots of if-else statements to handle the player and computer differently, and we also avoid code duplication for the player and computer. The tradeoff is that the player and computer must **share a common interface**. For `execute_turn()` to work, both `Player` and `Computer` must have a public `name` attribute and a public `guess()` method.\n", + "This pattern enables us to compress the game loop significantly; we avoiding writing lots of if-else statements to handle the player and computer differently, and we also avoid code duplication for the player and computer. The tradeoff is that the player and computer must **share a common interface**. For `execute_turn()` to work, both `Player` and `Computer` must have a public `name` attribute and a public `guess()` method. We can use a superclass to describe this interface, and implement any common functionality so that it does not need to be duplicated in the subclasses.\n", "\n", "`Player.guess()` will use the `input()` function to prompt the player for input, validating it and converting it into a tuple, while `Computer.guess()` will generate a random valid guess. Although both classes obey the same interface, they are implemented differently. This is the essence of the principle of **polymorphism**: different classes can be used interchangeably as long as they share a common interface." ] @@ -188,14 +223,14 @@ "\n", "Your code should have the following classes:\n", "\n", - "- `Grid`: The base class for the game board.\n", + "- `Grid`: The base class for `TrackingBoard` and `PlayingBoard`.\n", "- `TrackingBoard`: A subclass of `Grid` that tracks hits and misses.\n", "- `PlayingBoard`: A subclass of `Grid` that tracks ship positions.\n", - "- `Commander`: The base class for the player and computer.\n", + "- `Commander`: The base class for `Player` and `Computer`.\n", "- `Player`: A class that represents the player.\n", "- `Computer`: A class that represents the computer.\n", "\n", - "Introduce any necessary helper functions you require, and remove any unnecessary functions." + "Introduce any necessary helper functions you require, and remove any unnecessary functions. You may need to refactor some functions from the starting code to make them work with the new classes." ] }, { @@ -207,7 +242,7 @@ "\n", "In a separate `game.py` file, encapsulate the game loop in a `BattleshipsGame` class. The `BattleshipsGame` class should have the following public interface:\n", "\n", - "- `__init__(self, player_name: str)`: Initialises the game\n", + "- `__init__(self, player_name: str)`: Initialises the game and any attributes required by the class\n", "- `play(self)`: Starts the game loop\n", "- `is_gameover(self)`: Checks if the game is over\n", "\n", From 088b49e9611e91d6c69696f8b057b2d07d8c6101 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 15 Apr 2025 03:04:04 +0000 Subject: [PATCH 12/25] [fix] Format annotations for Python 3.7 compatibility --- Object-Oriented Programming/starting_code.py | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Object-Oriented Programming/starting_code.py b/Object-Oriented Programming/starting_code.py index ce2f26c..28f5dfc 100644 --- a/Object-Oriented Programming/starting_code.py +++ b/Object-Oriented Programming/starting_code.py @@ -9,14 +9,14 @@ VERTICAL = "vertical" -def create_grid(n: int, char: str) -> list[list[str]]: +def create_grid(n: int, char: str) -> "list[list[str]]": """Create a grid of size n x n filled with char, and return it.""" grid = [] for _ in range(n): grid.append([char] * n) return grid -def is_unoccupied(board: list[list[str]], row: int, col: int, size: int, orientation: str) -> bool: +def is_unoccupied(board: "list[list[str]]", row: int, col: int, size: int, orientation: str) -> bool: """Check if the specified area on the board is unoccupied. Return True if unoccupied, False otherwise. @@ -31,7 +31,7 @@ def is_unoccupied(board: list[list[str]], row: int, col: int, size: int, orienta return False return True -def place_ship_horizontally(board: list[list[str]], ship_name: str, size: int, row: int, col: int) -> None: +def place_ship_horizontally(board: "list[list[str]]", ship_name: str, size: int, row: int, col: int) -> None: """Place a ship horizontally on the board at the specified location. Return True if successful, False if not. @@ -41,7 +41,7 @@ def place_ship_horizontally(board: list[list[str]], ship_name: str, size: int, r for i in range(size): board[row][col + i] = ship_name -def place_ship_vertically(board: list[list[str]], ship_name: str, size: int, row: int, col: int) -> None: +def place_ship_vertically(board: "list[list[str]]", ship_name: str, size: int, row: int, col: int) -> None: """Place a ship vertically on the board at the specified location. Return True if successful, False if not. @@ -51,7 +51,7 @@ def place_ship_vertically(board: list[list[str]], ship_name: str, size: int, row for i in range(size): board[row + i][col] = ship_name -def place_ship(board: list[list[str]], ship_name: str, size: int) -> None: +def place_ship(board: "list[list[str]]", ship_name: str, size: int) -> None: """Place a ship of size size on the board at a random location.""" orientation = random.choice([HORIZONTAL, VERTICAL]) placed = False @@ -67,7 +67,7 @@ def place_ship(board: list[list[str]], ship_name: str, size: int) -> None: place_ship_vertically(board, ship_name, size, row, col) placed = True -def display_board(board: list[list[str]]) -> None: +def display_board(board: "list[list[str]]") -> None: """Display the current state of the board.""" # Column label row print(" " + " ".join(str(i) for i in range(GRID_SIZE))) @@ -75,7 +75,7 @@ def display_board(board: list[list[str]]) -> None: for i, row in enumerate(board): print(f"{i} " + " ".join(row)) -def is_valid_guess(board: list[list[str]], text: str) -> bool: +def is_valid_guess(board: "list[list[str]]", text: str) -> bool: """Check if the guess is valid. Return True if valid, False otherwise. A guess is expected in the form "row,col", where row and col are integers between 0 and GRID_SIZE - 1. @@ -94,7 +94,7 @@ def is_valid_guess(board: list[list[str]], text: str) -> bool: return False return True -def guess_to_coordinates(guess: str) -> tuple[int, int]: +def guess_to_coordinates(guess: str) -> "tuple[int, int]": """Convert a guess string to coordinates. Return the row and column as a tuple of integers. @@ -103,7 +103,7 @@ def guess_to_coordinates(guess: str) -> tuple[int, int]: x, y = int(first), int(second) return x, y -def prompt_valid_guess(board: list[list[str]]) -> tuple[int, int]: +def prompt_valid_guess(board: "list[list[str]]") -> "tuple[int, int]": """Prompt the user for a valid guess. If the guess is invalid, keep prompting until a valid guess is entered. @@ -115,7 +115,7 @@ def prompt_valid_guess(board: list[list[str]]) -> tuple[int, int]: guess = input("Invalid guess. Enter your guess (row,col): ") return guess_to_coordinates(guess) -def get_enemy_guess(board: list[list[str]]) -> tuple[int, int]: +def get_enemy_guess(board: "list[list[str]]") -> "tuple[int, int]": """Generate a random guess for the enemy. Return the row and column of the guess, as a tuple. @@ -127,18 +127,18 @@ def get_enemy_guess(board: list[list[str]]) -> tuple[int, int]: y = random.randint(0, GRID_SIZE - 1) return x, y -def is_target_hit(board: list[list[str]], x: int, y: int) -> bool: +def is_target_hit(board: "list[list[str]]", x: int, y: int) -> bool: """Check if the target hit a ship. Return True if it did, False otherwise.""" return board[x][y] != EMPTY -def targetting_update(board: list[list[str]], hit_what: str, x: int, y: int) -> None: +def targetting_update(board: "list[list[str]]", hit_what: str, x: int, y: int) -> None: """Update the targetting board with the guess.""" if hit_what == EMPTY: board[x][y] = MISS else: board[x][y] = HIT -def player_update(board: list[list[str]], x: int, y: int) -> None: +def player_update(board: "list[list[str]]", x: int, y: int) -> None: """Update the player board with the guess.""" if board[x][y] == EMPTY: board[x][y] = MISS @@ -157,7 +157,7 @@ def is_gameover(turns: int, player_hits: int, enemy_hits: int, total_ship_cells: return True return False -def display_overlay(targetting: list[list[str]], playing: list[list[str]]) -> None: +def display_overlay(targetting: "list[list[str]]", playing: "list[list[str]]") -> None: """Display the targetting overlaid on the ship board.""" # Column label row print(" " + " ".join(str(i) for i in range(GRID_SIZE))) @@ -173,7 +173,7 @@ def display_overlay(targetting: list[list[str]], playing: list[list[str]]) -> No print(playing[i][j], end=" ") print() # end the row -def run_game(turns: int, ships: dict[str, int]) -> None: +def run_game(turns: int, ships: "dict[str, int]") -> None: # Variables for tracking game state player_hits = 0 enemy_hits = 0 From b4ba98496252ed962878bff436097f7427a3de32 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 15 Apr 2025 03:35:16 +0000 Subject: [PATCH 13/25] [typo] fix typo --- Object-Oriented Programming/01 Encapsulation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index d88c030..5c9f5cb 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -517,10 +517,10 @@ " self.grid_size = grid_size\n", "\n", " def get(self, x: int, y: int) -> str:\n", - " return self.grid[x][y]\n", + " return self._grid[x][y]\n", " \n", " def set(self, x: int, y: int, char: str) -> None:\n", - " self.grid[x][y] = char\n", + " self._grid[x][y] = char\n", "\n", " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", " if orientation == HORIZONTAL:\n", From 28109e73e2073c5ae2fd98556be5f04996f0f7d7 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 15 Apr 2025 04:28:56 +0000 Subject: [PATCH 14/25] [format] Minor formatting; added summary --- .../01 Encapsulation.ipynb | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 5c9f5cb..8d235e0 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -198,7 +198,9 @@ "id": "a59065f0", "metadata": {}, "source": [ - "Python offers an even easier way to create a prepared object: instead of having to write a separate factory function (which is also easy to mix up with other factory functions), it lets us **bundle the factory function into the class**. Such bundled factory functions are called **constructor methods**. Just as an attribute is a variable bound to an object, a **method** is a function bound to an object. Attributes and methods must be accessed through the object, using `object.attribute` and `object.method()` syntax.\n", + "Python offers an even easier way to create a prepared object: instead of having to write a separate factory function (which is also easy to mix up with other factory functions), it lets us **bundle the factory function into the class**. Such bundled factory functions are called **constructor methods**.\n", + "\n", + "Just as an attribute is a variable bound to an object, a **method** is a function bound to an object. Attributes and methods must be accessed through the object, using `object.attribute` and `object.method()` syntax.\n", "\n", "In Python, we define a constructor method as follows:" ] @@ -464,7 +466,7 @@ "\n", "#### Using the public interface\n", "\n", - "Let's rewrite two functions, `is_target_hit()` and `targetting update()` to only use the public interface, without touching `Grid.grid`:" + "Below, I have rewritten two functions, `is_target_hit()` and `targetting update()` to only use the public interface, without touching `Grid.grid`:" ] }, { @@ -493,7 +495,7 @@ "id": "b18d814c", "metadata": {}, "source": [ - "Now we have eliminated any assumption about the grid's internal representation and format. We use the **interface**--the public `get()` and `set()` methods--to update the board data, which lets us use the board without having to know anything about its **implementation**--the `_grid` attribute. This is a good design, because it allows us to change the implementation without breaking the code that uses it.\n", + "Now we have eliminated any assumption about the grid's internal representation and format. We use the **interface**—the public `get()` and `set()` methods—to update the board data, which lets us use the board without having to know anything about its **implementation**—the `_grid` attribute. This is a good design, because it allows us to change the implementation without breaking the code that uses it.\n", "\n", "For example, our `Grid` might look like this now:" ] @@ -609,6 +611,44 @@ "source": [ "# Write your code here" ] + }, + { + "cell_type": "markdown", + "id": "daab1941", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "Research shows that **active recall**, the mental effort of attempting to remember, helps strengthen neuron connections. For each of the questions below, try to recall what you learnt from this lesson before you click to reveal.\n", + "\n", + "
    \n", + "\n", + "
  1. \n", + " What is an object? How is it related to a class? (click to reveal)\n", + "

    An object bundles related attributes and methods. It is instantiated from a class.

    \n", + "

    A class is a blueprint used to instantiate an object.

    \n", + "
  2. \n", + " \n", + "
  3. \n", + " What is an attribute? How does it differ from a variable? (click to reveal)\n", + "

    An attribute is a variable bound to an object. It must be accessed through the object (using object.attribute syntax).

    \n", + "
  4. \n", + "\n", + "
  5. \n", + " What is a method? How does it differ from a function? (click to reveal)\n", + "

    A method is a function bound to an object. It must be accessed through the object (using object.method() syntax).

    \n", + "
  6. \n", + "\n", + "
  7. \n", + " What is encapsulation? (click to reveal)\n", + "

    Encapsulation refers to the bundling of related attributes and methods into an object.

    \n", + "
  8. \n", + "\n", + "
  9. \n", + " How does encapsulation prevent inadvertent modification of code? (click to reveal)\n", + "

    Encapsulation allows separation of interface from implementation. Protected data is represented as private attributes, which may only be accessed through public methods. The public methods provide safe ways to modify the protected data while maintaining consistency and integrity of data.

    \n", + "
  10. " + ] } ], "metadata": { From 798d2556ed68f503ecdc076f1c1cc456f30c0119 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 15 Apr 2025 04:58:02 +0000 Subject: [PATCH 15/25] [format] Add summary --- .../02 Inheritance.ipynb | 27 +++++++++++++++++++ .../03 Polymorphism.ipynb | 22 +++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index bacbe5c..7e84d78 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -296,6 +296,33 @@ "source": [ "# Write your code for Exercise 2 here." ] + }, + { + "cell_type": "markdown", + "id": "183c693e", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "Research shows that **active recall**, the mental effort of attempting to remember, helps strengthen neuron connections. For each of the questions below, try to recall what you learnt from this lesson before you click to reveal.\n", + "\n", + "
      \n", + "\n", + "
    1. \n", + " What is inheritance? (click to reveal)\n", + "

      Inheritance refers to the ability of child classes to access public methods of the parent class.

      \n", + "
    2. \n", + "\n", + "
    3. \n", + " Why can't child classes access public attributes of a parent class? (click to reveal)\n", + "

      Attributes are bound to objects, not to classes. The notion of inheritance applies to classes, not objects.

      \n", + "
    4. \n", + "\n", + "
    5. \n", + " How does inheritance promote code reuse? (click to reveal)\n", + "

      Inheritance allows child classes to access parent class methods instead of reimplementing them. They may also extend those methods. This allows code written in the parent class to be written once and invoked wherever it is needed.

      \n", + "
    6. " + ] } ], "metadata": { diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb index 3a4a694..4547e21 100644 --- a/Object-Oriented Programming/03 Polymorphism.ipynb +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -233,6 +233,28 @@ "Introduce any necessary helper functions you require, and remove any unnecessary functions. You may need to refactor some functions from the starting code to make them work with the new classes." ] }, + { + "cell_type": "markdown", + "id": "67567aa1", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "Research shows that **active recall**, the mental effort of attempting to remember, helps strengthen neuron connections. For each of the questions below, try to recall what you learnt from this lesson before you click to reveal.\n", + "\n", + "
        \n", + "\n", + "
      1. \n", + " What is polymorphism? (click to reveal)\n", + "

        Polymorphism is the ability of methods with the same name in different classes to exhibit different behaviours at runtime.

        \n", + "
      2. \n", + "\n", + "
      3. \n", + " How does polymorphism promote code generalisation? (click to reveal)\n", + "

        Polymorphic classes present the same interface, allowing them to be used by the same code (without requiring conditional treatment, i.e. `if-else` statements).

        \n", + "
      4. " + ] + }, { "cell_type": "markdown", "id": "b987f527", From 766d8ee140a4495d722a355849b2a1cda1ed23ce Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 17 Apr 2025 07:20:37 +0000 Subject: [PATCH 16/25] [typo] fix typos --- Object-Oriented Programming/01 Encapsulation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 8d235e0..c329cb9 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -255,7 +255,7 @@ "#### Before\n", "\n", "```python\n", - "ef place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", + "def place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", " \"\"\"Place a ship of size size on the board at a random location.\"\"\"\n", " orientation = random.choice([HORIZONTAL, VERTICAL])\n", " placed = False\n", @@ -275,7 +275,7 @@ "#### After\n", "\n", "```python\n", - "def place_ship(board: list[list[str]], ship_name: str, size: int) -> None:\n", + "def place_ship(board: Grid, ship_name: str, size: int) -> None:\n", " \"\"\"Place a ship of size size on the board at a random location.\"\"\"\n", " orientation = random.choice([HORIZONTAL, VERTICAL])\n", " placed = False\n", From 916d14a8a4602e369c45b00749aefe6da7541bf8 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 17 Apr 2025 08:06:30 +0000 Subject: [PATCH 17/25] [refactor] JavaGrid provides mutator instead of setter --- Object-Oriented Programming/01 Encapsulation.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index c329cb9..15f8094 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -409,11 +409,11 @@ " self._grid = create_grid(grid_size, char)\n", " self._grid_size = grid_size\n", " \n", - " def get_grid(self) -> list[list[str]]:\n", - " return self._grid\n", + " def get_grid(self, x: int: y: int) -> str:\n", + " return self._grid[x][y]\n", "\n", - " def set_grid(self, grid: list[list[str]]) -> None:\n", - " self._grid = grid\n", + " def set_grid(self, x: int, y: int, value: str) -> None:\n", + " self._grid = grid[x][y] = value\n", " \n", " def get_grid_size(self) -> int:\n", " return self._grid_size\n", From e2a867fa2f63aa1a9188da8f2379e660dd6aaf1e Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 17 Apr 2025 08:11:08 +0000 Subject: [PATCH 18/25] [typo] fix typo --- Object-Oriented Programming/01 Encapsulation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Object-Oriented Programming/01 Encapsulation.ipynb b/Object-Oriented Programming/01 Encapsulation.ipynb index 15f8094..66bca50 100644 --- a/Object-Oriented Programming/01 Encapsulation.ipynb +++ b/Object-Oriented Programming/01 Encapsulation.ipynb @@ -566,10 +566,10 @@ " self.grid_size = grid_size\n", "\n", " def get(self, x: int, y: int) -> str:\n", - " return self.grid[x * self.grid_size + y]\n", + " return self._grid[x * self.grid_size + y]\n", " \n", " def set(self, x: int, y: int, char: str) -> None:\n", - " self.grid[x * self.grid_size + y] = char\n", + " self._grid[x * self.grid_size + y] = char\n", "\n", " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", " if orientation == HORIZONTAL:\n", From ebf42748a52c02d37b17340e1a69f5ef9a24dc24 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 01:55:15 +0000 Subject: [PATCH 19/25] Code updates to Grid: use 1D grid (to induce unfamiliarity with implementation), display() uses get() instead of direct access --- .../02 Inheritance.ipynb | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index 7e84d78..9015a50 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -33,9 +33,11 @@ "\n", "Yet one problem remains. We actually have two kinds of boards: a targetting board that tracks hits and misses (using `X` and `O`), and a playing board that tracks ship positions (using ship letters). Mixing up the two kinds of boards could be catastrophic.\n", "\n", - "The player and enemy tracking and playing boards now all use `Grid`, and the use of methods like `Grid.display()` prevents us from passing the wrong board to the display method. However, functions like `display_overlay(targetting: Grid, playing: Grid)` which take two grids may still have their arguments accidentally swapped.\n", + "The player and enemy tracking and playing boards now all use `Grid`, and the use of methods like `Grid.display()` prevents us from passing the wrong board to the display method. However, functions like `display_overlay(targetting: Grid, playing: Grid)` which take two grids may still have their arguments accidentally swapped. Furthermore, if we bundle methods like `place_ship()` and `update_tracking()` into the `Grid` class, we can accidentally call them on the wrong board. For example, if we call `place_ship()` on a tracking board, it will not work as expected.\n", "\n", - "In this lesson, we will create `TrackingBoard` and `PlayingBoard` classes. These two classes bundle additional data and methods specific to their purpose, yet can still be used wherever `Grid` is used." + "This is a problem of **type safety**. We want to ensure that the right methods are called on the right objects. In Python, we can use **subclassing** to create new classes that inherit from existing classes. This allows us to create specialized versions of a class while still using the same public interface.\n", + "\n", + "In this lesson, we will create `TrackingBoard` and `PlayingBoard` subclasses. These two classes bundle additional data and methods specific to their purpose, yet can still be used wherever `Grid` is used." ] }, { @@ -46,29 +48,46 @@ "outputs": [], "source": [ "# Run this cell to make the Grid class available for subsequent cells.\n", - "from starting_code import HORIZONTAL, VERTICAL, EMPTY, HIT, MISS, create_grid\n", + "from starting_code import HORIZONTAL, VERTICAL, EMPTY, HIT, MISS\n", "\n", "class Grid:\n", " \"\"\"Represents a grid in Battleships.\"\"\"\n", " def __init__(self, grid_size, char: str):\n", + " self._grid = [char] * (grid_size * grid_size)\n", " self.grid_size = grid_size\n", - " self._grid = create_grid(grid_size, char)\n", "\n", " def get(self, x: int, y: int) -> str:\n", " \"\"\"Get the character at the specified coordinates.\"\"\"\n", - " return self._grid[y][x]\n", + " return self._grid[x * self.grid_size + y]\n", " \n", " def set(self, x: int, y: int, char: str) -> None:\n", " \"\"\"Set the character at the specified coordinates.\"\"\"\n", - " self._grid[y][x] = char\n", - "\n", + " self._grid[x * self.grid_size + y] = char\n", + "\n", + " def is_unoccupied(self, row: int, col: int, size: int, orientation: str) -> bool:\n", + " if orientation == HORIZONTAL:\n", + " for i in range(size):\n", + " if self.get(row, col + i) != EMPTY:\n", + " return False\n", + " elif orientation == VERTICAL:\n", + " for i in range(size):\n", + " if self.get(row + i, col) != EMPTY:\n", + " return False\n", + " return True\n", + " \n", " def display(self) -> None:\n", - " \"\"\"Display the current state of the board.\"\"\"\n", + " \"\"\"Display the grid.\"\"\"\n", " # Column label row\n", - " print(\" \" + \" \".join(str(i) for i in range(self.grid_size)))\n", + " print(\" \" + \" \".join(str(i) for i in range(GRID_SIZE)))\n", " # Row label followed by the row contents\n", - " for i, row in enumerate(self._grid):\n", - " print(f\"{i} \" + \" \".join(row))\n" + " # We can't just iterate over the grid, assuming it is a 2D list,\n", + " # because it might not be.\n", + " for i in range(self.grid_size):\n", + " # Row header\n", + " print(str(i) + \" \", end=\"\")\n", + " for j in range(self.grid_size):\n", + " print(self.get(i, j), end=\" \")\n", + " print() # linebreak" ] }, { @@ -80,20 +99,20 @@ "\n", "Looking at our starting code again, we can see `TrackingBoard` which tracks hits and misses will require the following attributes and methods:\n", "\n", - "- `hits`: int\n", + "- `hits`: `int ` \n", " Number of hits scored\n", - "- `targetting_update(hit_what: str, x: int, y: int)`\n", + "- `targetting_update(hit_what: str, x: int, y: int)` \n", " Update the targetting board with a hit or miss\n", "\n", "The `PlayingBoard` class will require the following attributes and methods:\n", "\n", - "- `ship_cells`: dict[str, int]\n", + "- `ship_cells`: `dict[str, int]` \n", " Number of cells remaining for each ship on the board\n", - "- `sunk_ships`: list[str]\n", + "- `sunk_ships`: `list[str]` \n", " List of sunk ships\n", - "- `is_target_hit(x: int, y: int) -> bool`\n", + "- `is_target_hit(x: int, y: int) -> bool` \n", " Check if the opponent scored a hit on this board\n", - "- `playing_update(x: int, y: int)`\n", + "- `playing_update(x: int, y: int)` \n", " Update the playing board with opponent's guesses\n", "\n", "Despite presenting a different set of attributes and methods, both classes will still need to be useable wherever `Grid` is used. This means we need to be able to call `get()`, `set()`, and `display()` on both classes, and pass them to functions that take a `Grid` as a parameter.\n", From e527459ff5e2239e95162eae56f696ec6db57f9d Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 02:30:12 +0000 Subject: [PATCH 20/25] [format] minor formatting updates --- Object-Oriented Programming/02 Inheritance.ipynb | 2 +- Object-Oriented Programming/03 Polymorphism.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index 9015a50..0634d91 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -48,7 +48,7 @@ "outputs": [], "source": [ "# Run this cell to make the Grid class available for subsequent cells.\n", - "from starting_code import HORIZONTAL, VERTICAL, EMPTY, HIT, MISS\n", + "from starting_code import GRID_SIZE, HORIZONTAL, VERTICAL, EMPTY, HIT, MISS\n", "\n", "class Grid:\n", " \"\"\"Represents a grid in Battleships.\"\"\"\n", diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb index 4547e21..8d99895 100644 --- a/Object-Oriented Programming/03 Polymorphism.ipynb +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -80,7 +80,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Copy your TargettingBoard and PlayingBoard class from the previous exercise into this cell\n", + "# Copy your Grid, TrackingBoard, and PlayingBoard classes from the previous exercise into this cell\n", "# and run this cell so it is available for the example code cell below.\n", "\n" ] From a13e9fab6d170db4a64bc828679b26bdd65c3fa6 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 02:55:26 +0000 Subject: [PATCH 21/25] [doc] Add method documentation --- Object-Oriented Programming/02 Inheritance.ipynb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index 0634d91..1a5aec0 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -242,7 +242,15 @@ " self.hits = 0\n", "\n", " def update(self, hit_what: str, x: int, y: int) -> None:\n", - " \"\"\"Update the grid with a hit or miss.\"\"\"\n", + " \"\"\"Update the grid with a hit or miss.\n", + " \n", + " Args:\n", + " hit_what (str): The character representing the hit or miss.\n", + " x (int): The x-coordinate of the hit or miss.\n", + " y (int): The y-coordinate of the hit or miss.\n", + " \"\"\"\n", + " # We expect hit_what to be either EMPTY or a ship character.\n", + " # If it is EMPTY, we have a miss.\n", " if hit_what == EMPTY:\n", " self.set(x, y, MISS)\n", " else:\n", From 59b31a38b99f42326d6b4c4ea051b0d474a94bb1 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 02:59:05 +0000 Subject: [PATCH 22/25] [doc] clarify task requirement --- Object-Oriented Programming/02 Inheritance.ipynb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index 1a5aec0..7118996 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -279,12 +279,15 @@ "## Exercise 1\n", "\n", "Implement the `PlayingBoard` class. It should inherit from `Grid`, and implement the following methods:\n", - "- `__init__(self, grid_size: int, char: str)`\n", - " Call `Grid.__init__()` to set the `_grid` and `grid_size` attributes, and set `ship_cells = {}` and `sunk_ships = []`.\n", - "- `is_target_hit(self, x: int, y: int) -> bool`\n", + "- `__init__(self, grid_size: int, char: str)` \n", + " - Call `Grid.__init__()` to set the `_grid` and `grid_size` attributes\n", + " - then set `ship_cells = {}` and `sunk_ships = []`.\n", + " - `ship_cells` is a `dict[str, int]` that tracks the unhit cells of each ship on the board. The keys are the ship letters, and the values are the number of cells remaining for that ship.\n", + " - `sunk_ships` is a `list[str]` of sunk ships.\n", + "- `is_target_hit(self, x: int, y: int) -> bool` \n", " Check if the opponent scored a hit on this board. If the cell is not empty, return `True`. Otherwise, return `False`.\n", - "- `update(self, x: int, y: int)` (renamed from `playing_update`)\n", - " Update the playing board with opponent's guesses. If the cell is not empty, remove the ship from `ship_cells` and add it to `sunk_ships`. Otherwise, do nothing.\n", + "- `update(self, x: int, y: int)` (renamed from `playing_update`) \n", + " Update the playing board with opponent's guesses. If the cell is not empty, decrement the ship's cell count from `ship_cells`. If the cell count is zero, add it to `sunk_ships`. Otherwise, do nothing.\n", "\n", "You may edit the starting code to implement the above. Remember to obey the public interface of the `Grid` class." ] From 5475ca31a448b3531355224114a1a31ed9526744 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 02:59:57 +0000 Subject: [PATCH 23/25] [format] formatting edits --- Object-Oriented Programming/02 Inheritance.ipynb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Object-Oriented Programming/02 Inheritance.ipynb b/Object-Oriented Programming/02 Inheritance.ipynb index 7118996..cc987be 100644 --- a/Object-Oriented Programming/02 Inheritance.ipynb +++ b/Object-Oriented Programming/02 Inheritance.ipynb @@ -280,14 +280,18 @@ "\n", "Implement the `PlayingBoard` class. It should inherit from `Grid`, and implement the following methods:\n", "- `__init__(self, grid_size: int, char: str)` \n", - " - Call `Grid.__init__()` to set the `_grid` and `grid_size` attributes\n", - " - then set `ship_cells = {}` and `sunk_ships = []`.\n", - " - `ship_cells` is a `dict[str, int]` that tracks the unhit cells of each ship on the board. The keys are the ship letters, and the values are the number of cells remaining for that ship.\n", - " - `sunk_ships` is a `list[str]` of sunk ships.\n", + " 1. Call `Grid.__init__()` to set the `_grid` and `grid_size` attributes\n", + " 2. Then set `ship_cells = {}` and `sunk_ships = []`.\n", + " 3. `ship_cells` is a `dict[str, int]` that tracks the unhit cells of each ship on the board. The keys are the ship letters, and the values are the number of cells remaining for that ship.\n", + " 4. `sunk_ships` is a `list[str]` of sunk ships.\n", "- `is_target_hit(self, x: int, y: int) -> bool` \n", - " Check if the opponent scored a hit on this board. If the cell is not empty, return `True`. Otherwise, return `False`.\n", + " 1. Check if the opponent scored a hit on this board.\n", + " 2. If the cell is not empty, return `True`. Otherwise, return `False`.\n", "- `update(self, x: int, y: int)` (renamed from `playing_update`) \n", - " Update the playing board with opponent's guesses. If the cell is not empty, decrement the ship's cell count from `ship_cells`. If the cell count is zero, add it to `sunk_ships`. Otherwise, do nothing.\n", + " 1. Update the playing board with opponent's guesses.\n", + " 2. If the cell is not empty, decrement the ship's cell count from `ship_cells`.\n", + " 3. If the cell count is zero, add it to `sunk_ships`.\n", + " 4. Otherwise, do nothing.\n", "\n", "You may edit the starting code to implement the above. Remember to obey the public interface of the `Grid` class." ] From ab53c9cbc062e0ce55a3625cd40faf54012d687d Mon Sep 17 00:00:00 2001 From: JS Ng Date: Tue, 22 Apr 2025 03:02:17 +0000 Subject: [PATCH 24/25] [typo] fix attribute typo --- Object-Oriented Programming/03 Polymorphism.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb index 8d99895..61b9238 100644 --- a/Object-Oriented Programming/03 Polymorphism.ipynb +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -104,7 +104,7 @@ " \"\"\"\n", " def __init__(self, name):\n", " self.name = name\n", - " self.targetting = TargettingBoard(GRID_SIZE, EMPTY)\n", + " self.tracking = TrackingBoard(GRID_SIZE, EMPTY)\n", " self.playing = PlayingBoard(GRID_SIZE, EMPTY)\n", "\n", " def guess(self) -> tuple[int, int]:\n", @@ -140,13 +140,13 @@ " x, y = attacker.guess()\n", "\n", " hit_char = defender.playing.get(x, y)\n", - " attacker.targetting.update(x, y, hit_char) # also updates attacker.targetting.hits\n", + " attacker.tracking.update(x, y, hit_char) # also updates attacker.tracking.hits\n", " defender.playing.update(x, y) # updates ship_cells and sunk_ships\n", " if defender.is_sunk(hit_char): # assuming the existence of this method; it can be implemented if not existent\n", " print(f\"{attacker} sunk the\", hit_char + \"!\")\n", " # Only the player needs to see the overlay\n", " if isinstance(attacker, Player):\n", - " display_overlay(attacker.targetting, defender.board)\n", + " display_overlay(attacker.tracking, defender.board)\n", "\n", "def run_game(turns: int, ships: dict[str, int]) -> None:\n", " total_ship_cells = sum(ships.values())\n", From 1f88a248c7d617bb8f9038a13e0980826965438a Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 23 Apr 2025 20:37:44 +0800 Subject: [PATCH 25/25] Update 03 Polymorphism.ipynb --- Object-Oriented Programming/03 Polymorphism.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Object-Oriented Programming/03 Polymorphism.ipynb b/Object-Oriented Programming/03 Polymorphism.ipynb index 61b9238..ef620d3 100644 --- a/Object-Oriented Programming/03 Polymorphism.ipynb +++ b/Object-Oriented Programming/03 Polymorphism.ipynb @@ -158,7 +158,7 @@ " board.place_ship(name, size)\n", " # ship_cells are updated within the place_ship() method\n", "\n", - " while not is_gameover(turns: int, player: Player, enemy: Computer, total_ship_cells: int):\n", + " while not is_gameover(turns, player, enemy, total_ship_cells):\n", " print(\"\\nTurns left:\", turns)\n", " # Use two execute_turn() function calls to execute a turn for each commander\n", " # by swapping the attacker and defender\n",