From 12a9e48dc629ba7d4bd82bbba82c94e6c2980e19 Mon Sep 17 00:00:00 2001 From: David Kopec Date: Fri, 3 May 2024 22:10:51 -0400 Subject: [PATCH] Finished code for Chapters 7 & 8; updated metadata --- .github/workflows/python-package.yml | 7 +- Brainfuck/__main__.py | 2 +- Brainfuck/brainfuck.py | 2 +- Chip8/__main__.py | 2 +- Chip8/vm.py | 2 +- KNN/__main__.py | 2 +- KNN/fish.py | 2 +- KNN/knn.py | 15 ++-- NESEmulator/__main__.py | 2 +- NESEmulator/cpu.py | 2 +- NESEmulator/ppu.py | 2 +- NESEmulator/rom.py | 2 +- NanoBASIC/__main__.py | 2 +- NanoBASIC/executioner.py | 2 +- NanoBASIC/interpreter.py | 2 +- NanoBASIC/nodes.py | 2 +- NanoBASIC/parser.py | 2 +- NanoBASIC/tokenizer.py | 2 +- README.md | 121 ++++++++++++++++----------- RetroDither/__main__.py | 2 +- RetroDither/dither.py | 2 +- RetroDither/macpaint.py | 2 +- StainedGlass/__main__.py | 2 +- StainedGlass/stainedglass.py | 34 ++++---- StainedGlass/svg.py | 2 +- pyproject.toml | 24 ++++++ requirements.txt | 1 - setup.py | 6 -- 28 files changed, 148 insertions(+), 102 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 976ad46..a66b4c3 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,14 +16,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.12", "3.13"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/Brainfuck/__main__.py b/Brainfuck/__main__.py index 3bf7374..d8d7d66 100644 --- a/Brainfuck/__main__.py +++ b/Brainfuck/__main__.py @@ -1,6 +1,6 @@ # Brainfuck/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/Brainfuck/brainfuck.py b/Brainfuck/brainfuck.py index c2323f3..ebeb602 100644 --- a/Brainfuck/brainfuck.py +++ b/Brainfuck/brainfuck.py @@ -1,6 +1,6 @@ # Brainfuck/brainfuck.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/Chip8/__main__.py b/Chip8/__main__.py index e664434..649e2fb 100644 --- a/Chip8/__main__.py +++ b/Chip8/__main__.py @@ -1,6 +1,6 @@ # Chip8/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/Chip8/vm.py b/Chip8/vm.py index 86ad57b..9b798d7 100644 --- a/Chip8/vm.py +++ b/Chip8/vm.py @@ -1,6 +1,6 @@ # Chip8/vm.py # From Fun Computer Science Projects in Python -# Copyright 2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/KNN/__main__.py b/KNN/__main__.py index 598de0d..d00f761 100644 --- a/KNN/__main__.py +++ b/KNN/__main__.py @@ -54,7 +54,7 @@ def run(): digit_pixels.fill(0) elif key_name == "p": # predict what the digit should look like pixels = digit_pixels.transpose((1, 0, 2))[:, :, 0].flatten() * P_TO_D - predicted_pixels = digits_knn.predict(K, Digit("", pixels), "pixels") + predicted_pixels = digits_knn.predict_array(K, Digit("", pixels), "pixels") predicted_pixels = predicted_pixels.reshape((PIXEL_HEIGHT, PIXEL_WIDTH)).transpose((1, 0)) * D_TO_P digit_pixels = np.stack((predicted_pixels, predicted_pixels, predicted_pixels), axis=2) # Handle mouse events diff --git a/KNN/fish.py b/KNN/fish.py index 2fceddd..e5fc246 100644 --- a/KNN/fish.py +++ b/KNN/fish.py @@ -35,4 +35,4 @@ def from_string_data(cls, data: list[str]) -> Self: def distance(self, other: Self) -> float: return ((self.length1 - other.length1) ** 2 + (self.length2 - other.length2) ** 2 + (self.length3 - other.length3) ** 2 + (self.height - other.height) ** 2 + - (self.width - other.width) ** 2) ** 0.5 \ No newline at end of file + (self.width - other.width) ** 2) ** 0.5 diff --git a/KNN/knn.py b/KNN/knn.py index bae5940..1c1bb59 100644 --- a/KNN/knn.py +++ b/KNN/knn.py @@ -15,8 +15,7 @@ # limitations under the License. import csv from typing import Protocol, Self -from pathlib import Path -from collections.abc import Iterable +import numpy as np class DataPoint(Protocol): @@ -29,13 +28,13 @@ def distance(self, other: Self) -> float: ... class KNN[DP: DataPoint]: - def __init__(self, data_point_type: type[DP], file_path: Path, has_header: bool = True) -> None: + def __init__(self, data_point_type: type[DP], file_path: str, has_header: bool = True) -> None: self.data_point_type = data_point_type self.data_points = [] self._read_csv(file_path, has_header) # Read a CSV file and return a list of data points - def _read_csv(self, file_path: Path, has_header: bool) -> None: + def _read_csv(self, file_path: str, has_header: bool) -> None: with open(file_path, 'r') as f: reader = csv.reader(f) if has_header: @@ -61,6 +60,12 @@ def classify(self, k: int, data_point: DP) -> str: # Predict a property of a data point based on the k nearest neighbors # Find the average of that property from the neighbors and return it - def predict(self, k: int, data_point: DP, property_name: str) -> float | Iterable: + def predict(self, k: int, data_point: DP, property_name: str) -> float: neighbors = self.nearest(k, data_point) return sum([getattr(neighbor, property_name) for neighbor in neighbors]) / len(neighbors) + + # Predict a property of a data point based on the k nearest neighbors + # Find the average of that property from the neighbors and return it + def predict_array(self, k: int, data_point: DP, property_name: str) -> np.ndarray: + neighbors = self.nearest(k, data_point) + return np.sum([getattr(neighbor, property_name) for neighbor in neighbors]) / len(neighbors) diff --git a/NESEmulator/__main__.py b/NESEmulator/__main__.py index b0d28b8..64ac698 100644 --- a/NESEmulator/__main__.py +++ b/NESEmulator/__main__.py @@ -1,6 +1,6 @@ # NESEmulator/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NESEmulator/cpu.py b/NESEmulator/cpu.py index 1ce4150..545233b 100644 --- a/NESEmulator/cpu.py +++ b/NESEmulator/cpu.py @@ -1,6 +1,6 @@ # NESEmulator/cpu.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NESEmulator/ppu.py b/NESEmulator/ppu.py index aded774..399082e 100644 --- a/NESEmulator/ppu.py +++ b/NESEmulator/ppu.py @@ -1,6 +1,6 @@ # NESEmulator/ppu.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NESEmulator/rom.py b/NESEmulator/rom.py index ddd728e..ea1490f 100644 --- a/NESEmulator/rom.py +++ b/NESEmulator/rom.py @@ -1,6 +1,6 @@ # NESEmulator/rom.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/__main__.py b/NanoBASIC/__main__.py index 7606687..97439cc 100644 --- a/NanoBASIC/__main__.py +++ b/NanoBASIC/__main__.py @@ -1,6 +1,6 @@ # NanoBASIC/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/executioner.py b/NanoBASIC/executioner.py index 0bb9fcc..f688387 100644 --- a/NanoBASIC/executioner.py +++ b/NanoBASIC/executioner.py @@ -1,6 +1,6 @@ # NanoBASIC/executioner.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/interpreter.py b/NanoBASIC/interpreter.py index 8d2335c..64ca95f 100644 --- a/NanoBASIC/interpreter.py +++ b/NanoBASIC/interpreter.py @@ -1,6 +1,6 @@ # NanoBASIC/interpreter.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/nodes.py b/NanoBASIC/nodes.py index e56230f..4eda186 100644 --- a/NanoBASIC/nodes.py +++ b/NanoBASIC/nodes.py @@ -1,6 +1,6 @@ # NanoBASIC/nodes.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/parser.py b/NanoBASIC/parser.py index 472bd05..81a7528 100644 --- a/NanoBASIC/parser.py +++ b/NanoBASIC/parser.py @@ -1,6 +1,6 @@ # NanoBASIC/parser.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/NanoBASIC/tokenizer.py b/NanoBASIC/tokenizer.py index a2cb872..09b143b 100644 --- a/NanoBASIC/tokenizer.py +++ b/NanoBASIC/tokenizer.py @@ -1,6 +1,6 @@ # NanoBASIC/tokenizer.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 518a6c6..5bc25ab 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Source for the book Fun Computer Science Projects in Python by [David Kopec](htt ## Authorship and License -The code in this repository is Copyright 2022 David Kopec and released under the terms of the Apache License 2.0. That means you can reuse the code, but you must give credit to David Kopec. +The code in this repository is Copyright 2024 David Kopec and released under the terms of the Apache License 2.0. That means you can reuse the code, but you must give credit to David Kopec. Please read the license for details. ## Running and Testing Each Project -The following directions assume you are in the root directory of the repository in a terminal and that your Python command is `python` (on some systems it is `python3`). You need Python 3.9+ installed. +The following directions assume you are in the root directory of the repository in a terminal and that your Python command is `python` (on some systems it is `python3`). The code is written against Python 3.12, although most of it will run with Python 3.9+. -### Brainfuck +### Brainfuck (Chapter 1) A simple [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) interpreter. @@ -31,27 +31,7 @@ For example: `python -m tests.test_brainfuck` -### Chip8 - -A Chip8 virtual machine. - -#### Requirements - -- PyGame - -#### Running - -`python -m Chip8 ` - -For example: - -`python -m Chip8 Chip8/Games/tetris.chip` - -#### Testing - -`python -m tests.test_chip8` - -### NanoBASIC +### NanoBASIC (Chapter 2) An interpreter for a very simple dialect of BASIC based on [Tiny BASIC](https://en.wikipedia.org/wiki/Tiny_BASIC). @@ -71,28 +51,7 @@ For example: `python -m tests.test_nanobasic` -### NESEmulator - -A simple [NES](https://en.wikipedia.org/wiki/Nintendo_Entertainment_System) emulator that can play some basic public domain games. - -#### Requirements - -- PyGame -- NumPy - -#### Running - -`python -m NESEmulator ` - -For example: - -`python -m NESEmulator NESEmulator/Games/LanMaster.nes` - -#### Testing - -`python -m tests.test_nesemulator` - -### RetroDither +### RetroDither (Chapter 3) Dithers images into 1 bit black & white and exports them to MacPaint format. @@ -112,7 +71,7 @@ Additional options: `-g` output a .gif format version as well -### StainedGlass +### StainedGlass (Chapter 4) Computationally draws abstract approximations of images using vector shapes. @@ -147,4 +106,70 @@ Additional options: `-v, --vector` Create vector output. A SVG file will also be output. `-a ANIMATE, --animate ANIMATE` If a number greater than 0 is provided, will create an animated GIF with the number of milliseconds per frame - provided. \ No newline at end of file + provided. + +### Chip8 (Chapter 5) + +A Chip8 virtual machine. + +#### Requirements + +- PyGame +- NumPy + +#### Running + +`python -m Chip8 ` + +For example: + +`python -m Chip8 Chip8/Games/tetris.chip` + +#### Testing + +`python -m tests.test_chip8` + +### NESEmulator (Chapter 6) + +A simple [NES](https://en.wikipedia.org/wiki/Nintendo_Entertainment_System) emulator that can play some basic public domain games. + +#### Requirements + +- PyGame +- NumPy + +#### Running + +`python -m NESEmulator ` + +For example: + +`python -m NESEmulator NESEmulator/Games/LanMaster.nes` + +"a" is Select, "s" is Start, "arrow keys" are the D-pad, "z" is B, and "x" is A. + +#### Testing + +`python -m tests.test_nesemulator` + +### KNN (Chapter 7) + +A handwritten digit recognizer using the K-nearest neighbors algorithm. + +#### Requirements + +- PyGame +- NumPy + +#### Running + +`python -m KNN` + +Then use the key commands "c" to classify, "p" to predict, and "e" to erase. + +#### Testing + +`python -m tests.test_knn` + +## Type Hints +The code in this repository uses the latest type hinting features in Python 3.12. If you are using an older version of Python, you may need to remove some of the type hints to run the code. All the type hints in the source code were checked using [Pyright](https://github.com/microsoft/pyright). \ No newline at end of file diff --git a/RetroDither/__main__.py b/RetroDither/__main__.py index 1630d68..a7d27e1 100644 --- a/RetroDither/__main__.py +++ b/RetroDither/__main__.py @@ -1,6 +1,6 @@ # RetroDither/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/RetroDither/dither.py b/RetroDither/dither.py index cc0040b..077f440 100644 --- a/RetroDither/dither.py +++ b/RetroDither/dither.py @@ -1,6 +1,6 @@ # RetroDither/dither.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/RetroDither/macpaint.py b/RetroDither/macpaint.py index a97b0a9..b3fc190 100644 --- a/RetroDither/macpaint.py +++ b/RetroDither/macpaint.py @@ -1,6 +1,6 @@ # RetroDither/macpaint.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/StainedGlass/__main__.py b/StainedGlass/__main__.py index b4e8a2a..f0cfdaf 100644 --- a/StainedGlass/__main__.py +++ b/StainedGlass/__main__.py @@ -1,6 +1,6 @@ # StainedGlass/__main__.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/StainedGlass/stainedglass.py b/StainedGlass/stainedglass.py index b02af69..447e6b1 100644 --- a/StainedGlass/stainedglass.py +++ b/StainedGlass/stainedglass.py @@ -1,6 +1,6 @@ # StainedGlass/stainedglass.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ def get_most_common_color(image: Image.Image) -> tuple[int, int, int]: colors = image.getcolors(image.width * image.height) - return max(colors, key=lambda item: item[0])[1] + return max(colors, key=lambda item: item[0])[1] # type: ignore class StainedGlass: @@ -45,7 +45,7 @@ def __init__(self, file_name: str, output_file: str, trials: int, method: ColorM width, height = self.original.size aspect_ratio = width / height new_size = (int(MAX_HEIGHT * aspect_ratio), MAX_HEIGHT) - self.original.thumbnail(new_size, Image.ANTIALIAS) + self.original.thumbnail(new_size, Image.Resampling.LANCZOS) # Start the generated image with a background that is the # average of all the original's pixels in color average_color = tuple((round(n) for n in ImageStat.Stat(self.original).mean)) @@ -114,7 +114,7 @@ def experiment() -> bool: new_image = original.copy() glass_draw = ImageDraw.Draw(new_image) if self.shape_type == ShapeType.ELLIPSE: - glass_draw.ellipse(coordinates, fill=color) + glass_draw.ellipse(self.bounding_box(coordinates), fill=color) else: # must be triangle or quadrilateral or line glass_draw.polygon(coordinates, fill=color) new_difference = self.difference(new_image) @@ -143,28 +143,26 @@ def create_output(self, output_file: str, height: int, vector: bool, animation_l output_size = (int(original_width * ratio), int(original_height * ratio)) output_image = Image.new("RGB", output_size, average_color) output_draw = ImageDraw.Draw(output_image) - if vector: - svg = SVG(*output_size, average_color) - if animation_length > 0: - animation_frames = [] + svg = SVG(*output_size, average_color) if vector else None + animation_frames = [] if animation_length > 0 else None for coordinate_list, color in self.shapes: coordinates = [int(x * ratio) for x in coordinate_list] if self.shape_type == ShapeType.ELLIPSE: - output_draw.ellipse(coordinates, fill=color) - if vector: - svg.draw_ellipse(*coordinates, color) + output_draw.ellipse(self.bounding_box(coordinates), fill=color) + if svg: + svg.draw_ellipse(*coordinates, color) # type: ignore else: # must be triangle or quadrilateral or line - if vector: + output_draw.polygon(coordinates, fill=color) + if svg: if self.shape_type == ShapeType.LINE: - svg.draw_line(*coordinates, color) + svg.draw_line(*coordinates, color) # type: ignore else: svg.draw_polygon(coordinates, color) - output_draw.polygon(coordinates, fill=color) - if animation_length > 0: + if animation_frames is not None: animation_frames.append(output_image.copy()) output_image.save(output_file) - if vector: + if svg: svg.write(output_file + ".svg") - if animation_length > 0: + if animation_frames is not None: animation_frames[0].save(output_file + ".gif", save_all=True, append_images=animation_frames[1:], - optimize=False, duration=animation_length, loop=0) + optimize=False, duration=animation_length, loop=0, transparency=0, disposal=2) diff --git a/StainedGlass/svg.py b/StainedGlass/svg.py index 9967eb6..54626dd 100644 --- a/StainedGlass/svg.py +++ b/StainedGlass/svg.py @@ -1,6 +1,6 @@ # StainedGlass/svg.py # From Fun Computer Science Projects in Python -# Copyright 2021-2022 David Kopec +# Copyright 2024 David Kopec # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9e3496a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "FunComputerScienceProjectsInPython" +authors = [{name = "David Kopec", email = "david@oaksnow.com"}] +version = "1.0.0" +description = "Source code from the book Fun Computer Science Projects in Python" +requires-python = ">=3.12" +readme = "README.md" +license = {file = "LICENSE"} +keywords = ["book", "computer science", "emulator", "interpreter", "compuer art", "macpaint", "knn", "basic", "brainfuck", "nes", "chip8"] +dependencies = [ + "Pillow >=10.3.0", + "NumPy >=1.26.4", + "Pygame >=2.5.2", +] + +[project.urls] +Home = "https://github.com/davecom/FunComputerScienceProjectsInPython" + +[tool.flit.module] +name = "funcomputerscienceprojectsinpython" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9ba55e7..a7cb780 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -setuptools~=68.2.2 Pillow~=10.3.0 pygame~=2.5.2 numpy~=1.26.4 diff --git a/setup.py b/setup.py deleted file mode 100644 index 5c2acbd..0000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name='FunComputerScienceProjectsInPython', - packages=find_packages(), -)