├── .gitattributes ├── .gitignore ├── update.sh ├── src ├── img │ ├── 1-1.png │ ├── 1-2.png │ ├── 10-1.png │ ├── 11.png │ ├── 3-3.png │ ├── 4-1.png │ ├── 5-1.png │ ├── 5-5.png │ ├── 6-1.png │ ├── 7-2.png │ ├── 7-3.png │ ├── 7-4.png │ ├── 8-4.png │ ├── 8-5.png │ ├── 9-2.png │ ├── 9-3.png │ ├── 9-4.png │ ├── 10-2-1.png │ ├── 10-2-2.png │ ├── final-render-2.png │ ├── final-render.png │ ├── progress-bar.png │ ├── rt │ │ ├── fig-1.08-light-bounce.svg │ │ ├── fig-1.05-sphere-normal.svg │ │ ├── fig-antialias.svg │ │ ├── fig-1.11-reflection.svg │ │ ├── fig-1.14-cam-view-geom.svg │ │ ├── fig-1.09-rand-vec.svg │ │ └── fig-1.03-cam-geom-2.svg │ ├── trpl04-01.svg │ ├── trpl04-05.svg │ ├── trpl04-02.svg │ ├── trpl04-04.svg │ └── trpl04-03.svg ├── SUMMARY.md ├── intro.md ├── ttt.md ├── ttt-solns.md ├── chip8-solns.md ├── 1.md └── chip8.md ├── README.md ├── book.toml ├── .github └── workflows │ └── deploy.yml └── LICENSE.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-detectable 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | .DS_STORE 3 | .vscode -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "${0%/*}" 3 | git pull 4 | mdbook build 5 | -------------------------------------------------------------------------------- /src/img/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/1-1.png -------------------------------------------------------------------------------- /src/img/1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/1-2.png -------------------------------------------------------------------------------- /src/img/10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/10-1.png -------------------------------------------------------------------------------- /src/img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/11.png -------------------------------------------------------------------------------- /src/img/3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/3-3.png -------------------------------------------------------------------------------- /src/img/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/4-1.png -------------------------------------------------------------------------------- /src/img/5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/5-1.png -------------------------------------------------------------------------------- /src/img/5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/5-5.png -------------------------------------------------------------------------------- /src/img/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/6-1.png -------------------------------------------------------------------------------- /src/img/7-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/7-2.png -------------------------------------------------------------------------------- /src/img/7-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/7-3.png -------------------------------------------------------------------------------- /src/img/7-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/7-4.png -------------------------------------------------------------------------------- /src/img/8-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/8-4.png -------------------------------------------------------------------------------- /src/img/8-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/8-5.png -------------------------------------------------------------------------------- /src/img/9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/9-2.png -------------------------------------------------------------------------------- /src/img/9-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/9-3.png -------------------------------------------------------------------------------- /src/img/9-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/9-4.png -------------------------------------------------------------------------------- /src/img/10-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/10-2-1.png -------------------------------------------------------------------------------- /src/img/10-2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/10-2-2.png -------------------------------------------------------------------------------- /src/img/final-render-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/final-render-2.png -------------------------------------------------------------------------------- /src/img/final-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/final-render.png -------------------------------------------------------------------------------- /src/img/progress-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWCS/rs118/HEAD/src/img/progress-bar.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RS118: UWCS Learns Rust 2 | 3 | A short 3-part intro to Rust course adapted from The Book, including CHIP-8 and Raytracing tutorials aimed at beginners. 4 | 5 | ## Related Repos: 6 | - [Tic Tac Toe](https://github.com/UWCS/rs118-tic-tac-toe) 7 | - [CHIP-8 Interpreter](https://github.com/UWCS/rs118-chip8) 8 | - [Raytracer](https://github.com/UWCS/rs118-raytracer) 9 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction ](./intro.md) 4 | 5 | - [Part 1 - Hello, Ferris!](./1.md) 6 | - [Part 2 - Fighting the Borrow Checker](./2.md) 7 | - [Part 3 - Don't panic](./3.md) 8 | - [Tic Tac Toe Project](./ttt.md) 9 | - [Solutions](./ttt-solns.md) 10 | - [CHIP-8 Project](./chip8.md) 11 | - [Solutions](./chip8-solns.md) 12 | - [Raytracer Project](./raytracer.md) 13 | - [Solutions](./raytracer-solns.md) 14 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Joey Harrison "] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "RS118: UWCS Learns Rust" 7 | 8 | [preprocessor.katex] 9 | 10 | [output.html] 11 | git-repository-url = "https://github.com/uwcs/rs118" 12 | preferred-dark-theme = "coal" 13 | preferred-light-theme = "rust" 14 | 15 | 16 | [output.html.fold] 17 | enable = true 18 | 19 | 20 | [output.html.playground] 21 | editable = true 22 | line-numbers = true 23 | 24 | [preprocessor.toc] 25 | command = "mdbook-toc" 26 | renderer = ["html"] 27 | marker = "[[TOC]]" 28 | max-level = 2 -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to site 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install SSH key 13 | uses: shimataro/ssh-key-action@v2 14 | with: 15 | key: ${{ secrets.SITES_SSH_PRIVATE_KEY }} 16 | name: id_ed25519 17 | known_hosts: ${{ vars.SSH_KNOWN_HOSTS }} 18 | config: ${{ vars.SSH_CONFIG }} 19 | if_key_exists: fail 20 | - name: Update Server 21 | run: ssh sites "chmod +x /srv/rs118/update.sh && /srv/rs118/update.sh" 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joey Harrison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Hello, and welcome to RS118! This is a short series of lectures and projects designed to introduce beginners to programming in Rust. There is a basic level of programming knowledge assumed throughout. 4 | 5 | This tutorial will heavily lean on and is adapted from [_The Rust Programming Language_](https://doc.rust-lang.org/book/), more commonly known as **The Book**. Book chapters and links are referenced throughout, and it is recommended you read the entire chapter, as these notes are just here as a brief summary. Other resources: 6 | 7 | - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) - If you're looking for an example for something to explain how to do it or how it works, look here 8 | - [The Reference](https://doc.rust-lang.org/stable/reference/) - This is a complete\* reference for the Rust language. It is quite detailed and technical, but very thorough. 9 | - [Rustlings](https://github.com/rust-lang/rustlings) - Lots of little exercises and examples to demonstrate specific concepts 10 | 11 | The code snippets in this book can be run using the play button in the top right corner: 12 | 13 | ```rust 14 | println!("Hello, Ferris!"); 15 | ``` 16 | 17 | Some can also be edited, to allow you to play around with the concepts being presented. Try to fix the error in the code below: 18 | 19 | ```rust, editable 20 | fn main() { 21 | let message: &str = "Edit me!" 22 | println!("Your message says: {message}"); 23 | } 24 | ``` 25 | 26 | I encourage you to play around with the snippets to help get a better understanding of how the compiler works. 27 | 28 | The source for this book is available [on GitHub](https://github.com/uwcs/rs118), and contributions/corrections/suggestions/additions are welcome. 29 | 30 | The full playlist of talks accompanying accompanying the notes can be [found here on youtube](https://www.youtube.com/playlist?list=PLM7py5yAB4FwxFfwamr9JPVDRKvGDUjvD). 31 | 32 | RS118 is kindly supported by an event grant from [The Rust Foundation](https://foundation.rust-lang.org/). They do a lot of really important stuff for the language, so go show them some love. 33 | 34 | This book and all the code associated with RS118 is distributed under the terms of the MIT licence, Copyright 2022 Joey Harrison & The University of Warwick Computing Society. If you use anything from this book or the associated GitHub repos, please give credit. 35 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.08-light-bounce.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/trpl04-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | s1 7 | 8 | name 9 | 10 | value 11 | 12 | ptr 13 | 14 | len 15 | 16 | 5 17 | 18 | capacity 19 | 20 | 5 21 | 22 | 23 | 24 | index 25 | 26 | value 27 | 28 | 0 29 | 30 | h 31 | 32 | 1 33 | 34 | e 35 | 36 | 2 37 | 38 | l 39 | 40 | 3 41 | 42 | l 43 | 44 | 4 45 | 46 | o 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/img/trpl04-05.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | s 7 | 8 | name 9 | 10 | value 11 | 12 | ptr 13 | 14 | 15 | 16 | 17 | s1 18 | 19 | name 20 | 21 | value 22 | 23 | ptr 24 | 25 | len 26 | 27 | 5 28 | 29 | capacity 30 | 31 | 5 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | index 40 | 41 | value 42 | 43 | 0 44 | 45 | h 46 | 47 | 1 48 | 49 | e 50 | 51 | 2 52 | 53 | l 54 | 55 | 3 56 | 57 | l 58 | 59 | 4 60 | 61 | o 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/ttt.md: -------------------------------------------------------------------------------- 1 | # Tic-Tac-Toe! 2 | 3 | We're gonna use everything we learned to put together a neat little game of tic-tac-toe. I'm assuming you all know the rules. If you get stuck on any of the tasks, try to use your resources (The Book, Rust by Example, Google), or ask for someone to help talk you through it, before going straight to the solutions. Remember, the compiler is your friend and will try to tell you where to fix your code when you have an error, and always run `cargo clippy` and `cargo fmt`! (I recommend setting up VS Code to do this for you on save) 4 | 5 | ## Task 0: `cargo new` 6 | 7 | Create a new Cargo project, `cd` into it, and open your editor. Check [The Book](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html) if you need a reminder on how to do this. 8 | 9 | ## Task 1: Data Types 10 | 11 | We're gonna need some types to represent things within our game. In a game with two players and a board, a `Player` type and a `Board` type both seem sensible. 12 | 13 | - There are two players, `X` and `O` 14 | - The board is a 3x3 grid of either an `X` or `O`, or blank. 15 | 16 | ### Task 1.1 17 | 18 | Implement a simple data type to represent players. We could just use strings or numbers to do this, but structuring data properly is key to writing good Rust. 19 | 20 | Recall that we can use `struct`s and `enum`s to create our own data types. Which of these could be used to represent our a type with only two different values? 21 | 22 | For any `struct` or `enum` you write today, add the line `#[derive(Copy, Clone)]` just above it. This [derive statement](https://doc.rust-lang.org/rust-by-example/trait/derive.html) tells the compiler to copy your types whenever it would usually move it, allowing us to (mostly) ignore the borrow checker and focus on the basics for now. Don't worry too much about what this does exactly for now, [but details are available here if you're interested](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#clone-and-copy-for-duplicating-values). 23 | 24 | ### Task 1.2 25 | 26 | Implement a simple data type to represent the state of the game/board. 27 | 28 | There are a few ways of approaching this, most of them involving a fixed size array. You'll want to use your player type, but also think about what types can be used to represent something that may or may not be there (`Option`, anyone...?) 29 | 30 | ### Task 1.3 31 | 32 | So we have some players and a game board, but what now? Well it's no good if neither of our players can see the board, so you're going to have to come up with some way of printing the board to the terminal. 33 | 34 | Create a new, empty instance of your game board type in main, and write some code to print the empty board. Experiment with manually adding some moves to the board and make sure your code can handle printing `X`s and `O`s properly. 35 | 36 | You'll most likely want to iterate through your board array in some way, printing some other characters along with it. You'll need some code to print your `Player` type too (using the [`Display` trait](https://doc.rust-lang.org/rust-by-example/hello/print/print_display.html) if you feel fancy), but a simple `match` expression with some `println!()`s will likely do for now.) 37 | 38 | **Note:** Rust might not allow you to compare the equality of two custom types so easily. This is also _A Good Thing™_ because the notion of equality is not so simple for all types, so much so that Rust splits it into two traits, `Eq` and `PartialEq`. You will probably need to derive them for your type, by adding `Eq, PartialEq` alongside `Copy, Clone` in the `#[derive()]` attribute. [Again, The Book has more details on this](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#partialeq-and-eq-for-equality-comparisons). 39 | 40 | ## Task 2: Gaming 41 | 42 | You're finally ready to write your game. You'll want a game loop in your main function to do a few things: 43 | 44 | - Print the state of the board each turn (T1.3) 45 | - Prompt a player for input (T2.1) 46 | - Add the player's guess to the board (T2.2) 47 | - Check if they've won (T3) 48 | - Move to the next turn (T2.3) 49 | 50 | The first bit you've already written, but we need to do the rest too 51 | 52 | ### Task 2.1 53 | 54 | Write some code to prompt for user input in a loop, storing whatever data they enter. 55 | 56 | What kind of loop do you want (`for`/`while`/`loop`), and when do you want to break out of it/jump back to the top of it? Consider your control flow carefully here. You'll also need some way to read user input from the terminal. Rust has a [`Stdin` struct](https://doc.rust-lang.org/std/io/fn.stdin.html) with a `read_line()` method in its standard library. [The Book](https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html#processing-a-guess) has some good examples of this. You'll need to convert your input from a string into a number too, so check out [`str::parse`](https://doc.rust-lang.org/std/primitive.str.html#method.parse) for some help with that. 57 | 58 | ### Task 2.2 59 | 60 | Now we have player input, we need to use it to update the state of the game. Use the input to add the player's turn to the board, if it is a valid guess. Have a look at `std::io` for input. Numbering squares left-to-right top-to-bottom works well, but if you want to be fancy, how about some chess board style labelling? 61 | 62 | What constitutes valid input for a turn? You have 9 squares on your game board, and you can't play where there is already a square in that space. If the guess isn't valid, you'll need to get the player to input a new guess. 63 | 64 | ### Task 2.3 65 | 66 | At the end of each turn, you need to move the game to the next player. Add some code to make sure players take turns properly in your loop, and make sure your game is mostly coherent at this point. 67 | 68 | ## Task 3: A winner? 69 | 70 | Two players should be able to play your game now, taking turns, and specifying only valid moves. But this is no fun if there are no winners. 71 | 72 | Add some code to your game loop to see if a move leads to the player winning. If so, print a message to indicate this, and exit the game. 73 | 74 | There are multiple cases to consider for a win: 3 rows, 3 columns, and the 2 diagonals. You could hard-code all 8 of these, or save some sanity with some `for` loops. Up to you. 75 | -------------------------------------------------------------------------------- /src/img/trpl04-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | s1 7 | 8 | name 9 | 10 | value 11 | 12 | ptr 13 | 14 | len 15 | 16 | 5 17 | 18 | capacity 19 | 20 | 5 21 | 22 | 23 | 24 | index 25 | 26 | value 27 | 28 | 0 29 | 30 | h 31 | 32 | 1 33 | 34 | e 35 | 36 | 2 37 | 38 | l 39 | 40 | 3 41 | 42 | l 43 | 44 | 4 45 | 46 | o 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | s2 55 | 56 | name 57 | 58 | value 59 | 60 | ptr 61 | 62 | len 63 | 64 | 5 65 | 66 | capacity 67 | 68 | 5 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/img/trpl04-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | s1 7 | 8 | name 9 | 10 | value 11 | 12 | ptr 13 | 14 | len 15 | 16 | 5 17 | 18 | capacity 19 | 20 | 5 21 | 22 | 23 | 24 | index 25 | 26 | value 27 | 28 | 0 29 | 30 | h 31 | 32 | 1 33 | 34 | e 35 | 36 | 2 37 | 38 | l 39 | 40 | 3 41 | 42 | l 43 | 44 | 4 45 | 46 | o 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | s2 55 | 56 | name 57 | 58 | value 59 | 60 | ptr 61 | 62 | len 63 | 64 | 5 65 | 66 | capacity 67 | 68 | 5 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/img/trpl04-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | s2 7 | 8 | name 9 | 10 | value 11 | 12 | ptr 13 | 14 | len 15 | 16 | 5 17 | 18 | capacity 19 | 20 | 5 21 | 22 | 23 | 24 | index 25 | 26 | value 27 | 28 | 0 29 | 30 | h 31 | 32 | 1 33 | 34 | e 35 | 36 | 2 37 | 38 | l 39 | 40 | 3 41 | 42 | l 43 | 44 | 4 45 | 46 | o 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | s1 55 | 56 | name 57 | 58 | value 59 | 60 | ptr 61 | 62 | len 63 | 64 | 5 65 | 66 | capacity 67 | 68 | 5 69 | 70 | 71 | 72 | index 73 | 74 | value 75 | 76 | 0 77 | 78 | h 79 | 80 | 1 81 | 82 | e 83 | 84 | 2 85 | 86 | l 87 | 88 | 3 89 | 90 | l 91 | 92 | 4 93 | 94 | o 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/ttt-solns.md: -------------------------------------------------------------------------------- 1 | # Tic-Tac-Toe Solutions 2 | 3 | ## Task 0 4 | 5 | If you haven't installed Rust/cargo already, head to 6 | 7 | ```bash 8 | cargo new tic-tac-toe 9 | cd tic-tac-toe 10 | code . 11 | ``` 12 | 13 | Exchange `code` for your editor of choice. 14 | 15 | ## Task 1.1 16 | 17 | You're looking for an enum: 18 | 19 | ```rust,noplayground 20 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 21 | enum Player { 22 | X, 23 | O, 24 | } 25 | ``` 26 | 27 | Note that I've [`derive`](https://doc.rust-lang.org/book/ch05-02-example-structs.html?highlight=derive#adding-useful-functionality-with-derived-traits)d some traits on this type, which will come in handy later: 28 | 29 | - `Debug` generates a string representation of the type for debugging, that can be used via the `dbg!()` macro 30 | - `Eq` and `PartialEq` are tell the compiler it can compare equality of the type using `==` and `!=` 31 | - `Copy` and `Clone` tells the compiler that it is free to copy the type all over the place 32 | - This means we don't have to worry about move semantics for now. That's a lesson for next time. 33 | 34 | ## Task 1.2 35 | 36 | There's a few different ways to go about this. You could use either a 2d array, or 1d array, whichever you prefer. The way I opted to use a 2-d array of `Option`, which represents either `Some(Player)`, or `None`, when the square is empty. I then wrapped that array in a struct, which also holds who's turn it currently is, and if there is currently a winner. This allows the struct to represent more the state of the entire game than just the board, but this is entirely up to you. 37 | 38 | ```rust,noplayground 39 | struct Board { 40 | grid: [[Option; 3]; 3], 41 | current_turn: Player, 42 | winner: Option, 43 | } 44 | ``` 45 | 46 | You could also opt to not hold the current player or winner in the struct, in which case a [type alias](https://doc.rust-lang.org/book/ch19-04-advanced-types.html?highlight=type%20alias#creating-type-synonyms-with-type-aliases) would make more sense. 47 | 48 | ```rust,noplayground 49 | type Board = [[Option; 3]; 3] 50 | ``` 51 | 52 | ## Task 1.3 53 | 54 | The `let mut` expression creates a new, mutable, instance of our `Board` struct from above. We then iterate through each square, adding in some decoration too. 55 | 56 | ```rust,noplayground 57 | let mut board = Board { 58 | grid: [[None, None, None], [None, None, None], [None, None, None]], 59 | current_turn: Player::X, 60 | winner: None, 61 | }; 62 | 63 | println!("-------------"); 64 | for row in board.grid { 65 | for square in row { 66 | print!("|"); 67 | match square { 68 | Some(p) => print!(" {} ", p), 69 | None => print!(" "), 70 | } 71 | } 72 | println!("|"); 73 | println!("-------------"); 74 | } 75 | ``` 76 | 77 | will print something like: 78 | 79 | ``` 80 | ------------- 81 | | X | O | X | 82 | ------------- 83 | | O | X | O | 84 | ------------- 85 | | X | O | X | 86 | ------------- 87 | ``` 88 | 89 | Note how we're using our `Player` enum within the `print!()` macro. This is because I manually implemented the `Display` trait on it: 90 | 91 | ```rust,noplayground 92 | impl Display for Player { 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | write!(f, "{}", 95 | match self { 96 | Player::X => "X", 97 | Player::O => "O", 98 | } 99 | ) 100 | } 101 | } 102 | ``` 103 | 104 | This may look a little scary for now, because it is. Pattern matching on `Option` in the loop just as good: 105 | 106 | ```rust,noplayground 107 | println!("-------------"); 108 | for row in board.grid { 109 | for square in row { 110 | print!("|"); 111 | match square { 112 | Some(Player::X) => print!(" {X} "), 113 | Some(Player::O) => print!(" {O} "), 114 | None => print!(" "), 115 | } 116 | } 117 | println!("|"); 118 | println!("-------------"); 119 | } 120 | ``` 121 | 122 | You could also implement the `Display` trait on the entire `Board` if you wished, moving the above code to the `fmt` function, similar to `Player`. 123 | 124 | ## Task 2.1 125 | 126 | You'll need `std::io`, which is a module from Rust's standard library. Modules are imported in Rust using the `use` keyword, so something like: 127 | 128 | ```rust,noplayground 129 | use std::io::stdin; 130 | use std::io::stdout; 131 | ``` 132 | 133 | will import those modules. You can also combine them if you want: 134 | 135 | ```rust,noplayground 136 | use std::io::{stdin, stdout}; 137 | ``` 138 | 139 | You could use a `while` loop, with a condition checking for winners, or a `loop` with a `break`. I opted for the latter approach. 140 | 141 | ```rust,noplayground 142 | fn main() { 143 | let mut board = Board { 144 | grid: [[None, None, None], [None, None, None], [None, None, None]], 145 | current_turn: Player::X, 146 | winner: None, 147 | }; 148 | 149 | loop { 150 | //prompt for user input 151 | print!("Player {}, enter a square>>", board.current_turn); 152 | //flush stdout because stdout is weird 153 | stdout().flush(); 154 | 155 | //create the buffer our input will be copied into 156 | let mut turn = String::new(); 157 | //read input into that buffer, 158 | stdin().read_line(&mut turn); 159 | 160 | //print the board 161 | println!("-------------"); 162 | for row in board.grid { 163 | for square in row { 164 | print!("|"); 165 | match square { 166 | Some(Player::X) => print!(" {X} "), 167 | Some(Player::O) => print!(" {O} "), 168 | None => print!(" "), 169 | } 170 | } 171 | println!("|"); 172 | println!("-------------"); 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | The reason we have to flush `stdout` manually is because it is usually flushed when there is a newline character or the runtime's buffer fills up, but neither of these things happen. 179 | 180 | Our board printing code is also included there at the bottom of the loop, but it doesn't do anything yet really, as using the input comes next. Our game is starting to come together! 181 | 182 | ## Task 2.2 183 | 184 | The following snippet validates the input is a number, in the range of the board, in a blank square. It then adds the turn to the board if so. 185 | 186 | ```rust,noplayground 187 | let guess: Result = turn.trim().parse(); 188 | 189 | if guess.is_err() { 190 | continue; 191 | } 192 | let square = guess.unwrap() - 1; 193 | let row = square / 3; 194 | let column = square % 3; 195 | 196 | if square > 8 || board.grid[row][column].is_some() { 197 | continue; 198 | } 199 | 200 | //add the turn to the board 201 | board.grid[row][column] = Some(board.current_turn); 202 | ``` 203 | 204 | [`parse()`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.parse) is a funny little function, as it is generic over any type that a string can be turned into, hence why we have to use the affectionately-named turbofish syntax to specify what type we want to parse our string into. It also returns a `Result`, which is like an upgraded version of `Option`, expressing that the function returns either our result `T`, or some type expressing an error `E`. We check that the `Result` is not an error, and then unwrap the guess from it. 205 | 206 | We use one conditional expression to check if our parse function failed using `is_err()`, then we can `unwrap()` our number from the `Result`. Another condition is used to check the square is not out of range, or already taken. We jump back to the top of the loop in any of these cases. 207 | 208 | ## Task 2.3 209 | 210 | We can just add a simple `match` expression at the bottom of our loop to switch turns. 211 | 212 | ```rust,noplayground 213 | board.current_turn = match board.current_turn { 214 | Player::O => Player::X, 215 | Player::X => Player::O, 216 | } 217 | ``` 218 | 219 | Depending upon how your loop works you might need to put this somewhere else to handle the control flow differently. 220 | 221 | Our main function now looks like this. I added a little help text at the top to print at the start of the game, too! 222 | 223 | ```rust,noplayground 224 | fn main() { 225 | println!("tic tac toe!"); 226 | println!("Board squares are numbered as follows:"); 227 | println!( 228 | "------------\n\ 229 | | 1 | 2 | 3 |\n\ 230 | -------------\n\ 231 | | 4 | 5 | 6 |\n\ 232 | -------------\n\ 233 | | 7 | 8 | 9 |\n\ 234 | -------------" 235 | ); 236 | 237 | let mut board = Board { 238 | grid: [[None, None, None], [None, None, None], [None, None, None]], 239 | current_turn: Player::X, 240 | winner: None, 241 | }; 242 | 243 | loop { 244 | print!("Player {}, enter a square>>", board.current_turn); 245 | stdout().flush().expect("Could not flush stdout"); 246 | 247 | let mut turn = String::new(); 248 | 249 | stdin().read_line(&mut turn).expect("Failed to read line"); 250 | let guess: Result = turn.trim().parse(); 251 | turn.clear(); 252 | 253 | if guess.is_err() { 254 | continue; 255 | } 256 | let square = guess.unwrap() - 1; 257 | if square > 8 || board.grid[square / 3][square % 3].is_some() { 258 | continue; 259 | } 260 | 261 | //print the board 262 | board.grid[square / 3][square % 3] = Some(board.current_turn); 263 | } 264 | } 265 | ``` 266 | 267 | ## Task 3 268 | 269 | This bit is a little more complicated. `Board.grid` is an array of `Option`, so we need to check that if each tile in the row is equal, and also that they are not all `None` values (done by the `is_some()` method). We check each row, each column, and also the two diagonals. If any of these checks end up storing a winner in `board.winner`, then the `match` at the bottom catches this and ends the game. 270 | 271 | ```rust,noplayground 272 | //check if we have any winners 273 | //check rows -- easily done 274 | for row in board.grid { 275 | if row[0] == row[1] && row[1] == row[2] && row[0].is_some() { 276 | board.winner = row[0]; 277 | } 278 | } 279 | //check columns -- need some indexing for this 280 | for i in 0..3_usize { 281 | if board.grid[0][i] == board.grid[1][i] 282 | && board.grid[1][i] == board.grid[2][i] 283 | && board.grid[0][i].is_some() 284 | { 285 | board.winner = board.grid[0][i]; 286 | } 287 | } 288 | //check diagonals 289 | if board.grid[0][0] == board.grid[1][1] 290 | && board.grid[1][1] == board.grid[2][2] 291 | && board.grid[0][0].is_some() 292 | { 293 | board.winner = board.grid[0][0]; 294 | } 295 | if board.grid[0][2] == board.grid[1][1] 296 | && board.grid[1][1] == board.grid[2][0] 297 | && board.grid[0][2].is_some() 298 | { 299 | board.winner = board.grid[0][2]; 300 | } 301 | ``` 302 | 303 | The `Eq` and `PartialEq` derives from earlier allow us to use the `==` operator to compare instances of our `Player` type. More info about those can be found [in The Book](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#partialeq-and-eq-for-equality-comparisons) 304 | 305 | ## The Final Product 306 | 307 | The full code can be found on github: 308 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.05-sphere-normal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/chip8-solns.md: -------------------------------------------------------------------------------- 1 | # CHIP-8 Solutions 2 | 3 | These solutions can be seen step-by-step on GitHub [here](https://github.com/ericthelemur/chip9/commits/master). 4 | 5 | ## Task 1.1 6 | 7 | Your directory structure should now look like this: 8 | 9 | ``` 10 | . 11 | ├── Cargo.lock 12 | ├── Cargo.toml 13 | └── src 14 | ├── interpreter 15 | │ └── mod.rs 16 | └── main.rs 17 | ``` 18 | 19 | Which gives a module hierarchy like this: 20 | 21 | ``` 22 | crate root 23 | └── main 24 | └── interpreter 25 | ``` 26 | 27 | You can add any other modules, for tests or anything else, anywhere you wish. 28 | 29 | ## Task 1.2 30 | 31 | The interpreter/CPU/virtual machine struct should look something like this: 32 | 33 | ```rust,noplayground 34 | pub struct ChipState { 35 | memory: [u8; 4096], 36 | program_counter: u16, 37 | registers: [u8; 16], 38 | display: chip8_base::Display, 39 | stack_pointer: u8, 40 | stack: [u16; 16], 41 | // ... there will be more 42 | } 43 | ``` 44 | 45 | Only a few of the fields you need are included here, you'll need to add a few more as you go, and you can represent them however you wish. The corresponding `new()` method should look like this: 46 | 47 | ```rust,noplayground 48 | impl ChipState { 49 | pub fn new() -> Self { 50 | Self { 51 | memory: [0; 4096], 52 | registers: [0; 16], 53 | program_counter: 0x200, 54 | display: [[chip8_base::Pixel::default(); 64]; 32], 55 | stack_pointer: 0, 56 | stack: [0; 16], 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | Note how both the type and the function are `pub`, so the module above (main, the crate root) can use them. The `program_counter` is initialized to `0x200`, as this is where CHIP-8 programs start. 63 | 64 | ## Task 1.3 & 1.4 65 | 66 | We implement the trait for the type like so 67 | 68 | ```rust,noplayground 69 | impl chip8_base::Interpreter for ChipState { 70 | fn step(&mut self, keys: &chip8_base::Keys) -> Option { 71 | todo!() 72 | } 73 | 74 | fn speed(&self) -> std::time::Duration { 75 | todo!() 76 | } 77 | 78 | fn buzzer_active(&self) -> bool { 79 | todo!() 80 | } 81 | } 82 | ``` 83 | 84 | Look at how the methods are capturing `self`. `step()` takes a mutable reference, because it needs to mutate the state of the virtual machine, but it doesn't move, because then we wouldn't be able to do more than one step. The other two take immutable references, because they only need to read state, not modify it. 85 | 86 | ## Task 1.5 87 | 88 | `main()` should look like this: 89 | 90 | ```rust,noplayground 91 | use interpreter::ChipState; 92 | 93 | mod interpreter; 94 | 95 | fn main() { 96 | let vm = ChipState::new(); 97 | 98 | chip8_base::run(vm); 99 | } 100 | 101 | ``` 102 | 103 | ## Task 1.6 104 | 105 | The following return values don't do anything, and let the interpreter run without panics: 106 | 107 | ```rust,noplayground 108 | use std::time::Duration; 109 | 110 | ... 111 | 112 | impl chip8_base::Interpreter for ChipState { 113 | fn step(&mut self, keys: &chip8_base::Keys) -> Option { 114 | None 115 | } 116 | 117 | fn speed(&self) -> Duration { 118 | Duration::from_secs(1) 119 | } 120 | 121 | fn buzzer_active(&self) -> bool { 122 | false 123 | } 124 | } 125 | ``` 126 | 127 | ## Task 1.7 128 | 129 | For a clock rate of 700Hz, you can create a `Duration` using `Duration::from_secs_f64(1_f64/700_f64)`. Don't hardcode this though. The "proper" way to do it is to modify your `new()` method to accept a clock speed, then store the duration in the struct to return when requested. 130 | 131 | ```rust,noplayground 132 | pub struct ChipState { 133 | ... 134 | speed: Duration 135 | } 136 | 137 | impl ChipState { 138 | pub fn new(clock_freq: u32) -> Self { 139 | Self { 140 | ... 141 | speed: Duration::from_secs_f64(1_f64 / clock_freq as f64), 142 | } 143 | } 144 | } 145 | 146 | ... 147 | 148 | impl Interpreter for ChipState { 149 | fn speed(&self) -> Duration { 150 | self.speed 151 | } 152 | } 153 | ``` 154 | 155 | ## Task 2.1 156 | 157 | ```rust,noplayground 158 | fn fetch(&mut self) -> u16 { 159 | let instruction = u16::from_be_bytes([ 160 | self.memory[self.program_counter as usize], 161 | self.memory[(self.program_counter + 1) as usize], 162 | ]); 163 | self.program_counter += 2; 164 | instruction 165 | } 166 | 167 | 168 | ``` 169 | 170 | We're capturing by mutable reference, because we need to mutate, but not take ownership. 171 | 172 | Look at the [documentation for the `from_be_bytes()`](https://doc.rust-lang.org/stable/std/primitive.u16.html#method.from_be_bytes) method if you don't get what's going on. 173 | 174 | There's lots of casting using `as usize` going on, because only a `usize` type can be used to index an array for safety reasons (imagine you used a `u16` type to index an array of 30,000 numbers, it wouldn't make sense semantically). Casting the program counter and other numbers to `usize` is gonna happen a lot, but you can't store them as `usize` types because that wouldn't make sense either, and would also make it much harder to keep track of what a value is meant to represent. 175 | 176 | ## Task 2.2 177 | 178 | The `self.program_counter & 0x0fff;` will wrap the program counter to 12 bits, discarding the upper nibble. Adding some debug calls too: 179 | 180 | ```rust,noplayground 181 | fn fetch(&mut self) -> u16 { 182 | dbg!(&self.program_counter); 183 | let instruction = u16::from_be_bytes([ 184 | self.memory[self.program_counter as usize], 185 | self.memory[(self.program_counter + 1) as usize], 186 | ]); 187 | self.program_counter += 2; 188 | self.program_counter & 0x0FFF; 189 | dbg!(&instruction); 190 | instruction 191 | } 192 | ``` 193 | 194 | We don't have to add any additional info to `dbg!()` because the expression and line number are printed for us. 195 | 196 | ## Task 2.3 197 | 198 | Our main should now look like this: 199 | 200 | ```rust, noplayground 201 | fn main() { 202 | env_logger::init(); 203 | 204 | let vm = ChipState::new(700); 205 | 206 | chip8_base::run(vm); 207 | } 208 | ``` 209 | 210 | Don't forget to add the crates to `Cargo.toml`. Where you choose to add logs is up to you, but as a rule of thumb, put a `log::debug!()` call everywhere you expect something might go wrong. You can use format strings in log macros too, just like `println!()`. 211 | 212 | ## Task 2.4 & 2.5 213 | 214 | First, we've written a helper method to break the `u16` instruction down into four nibbles (add in `impl ChipState`): 215 | 216 | ```rust,noplayground 217 | //break a u16 into its nibbles 218 | fn nibbles(n: u16) -> (u8, u8, u8, u8) { 219 | let n3 = ( n >> 12) as u8; 220 | let n2 = ((n >> 8) & 0b1111) as u8; 221 | let n1 = ((n >> 4) & 0b1111) as u8; 222 | let n0 = ( n & 0b1111) as u8; 223 | (n3, n2, n1, n0) 224 | } 225 | ``` 226 | 227 | We can then match on this. Below shows NOP (`0000`), AND (`8xy2`) and RET (`00EE`) implemented. Here, you could implement almost anything, but this is just an example of the sort of structure you need. 228 | 229 | ```rust,noplayground 230 | fn execute(&mut self, instruction: u16) { 231 | match Self::nibbles(instruction) { 232 | // 0000 NOP: Nothing 233 | (0x0, 0x0, 0x0, 0x0) => (), 234 | // 00EE RET: Return from subroutine 235 | (0x0, 0x0, 0xE, 0xE) => { 236 | self.program_counter = self.stack[self.stack_pointer as usize]; 237 | self.stack_pointer -= 1; 238 | }, 239 | // 8xy2 AND Vx, Vy: Set Vx = Vx AND Vy. 240 | (8, x, y, 2) => self.registers[x as usize] &= self.registers[y as usize], 241 | _ => panic!("Instruction either doesn't exist or hasn't been implemented yet"), 242 | } 243 | } 244 | ``` 245 | 246 | Note how we can specify constants in the tuple for the pattern, and also variables to bind to if the pattern matches. How you decode operands wider than one nibble is up to you. 247 | 248 | `step()` now looks like this: 249 | 250 | ```rust,noplayground 251 | fn step(&mut self, keys: &Keys) -> Option { 252 | let instr = self.fetch(); 253 | self.execute(instr); 254 | None 255 | } 256 | ``` 257 | 258 | ## Task 3.1 259 | 260 | ```rust,noplayground 261 | fn execute(&mut self, instruction: u16) -> Option { 262 | match Self::nibbles(instruction) { 263 | // 0000 NOP: Nothing 264 | (0x0, 0x0, 0x0, 0x0) => (), 265 | // 00E0 CLS: Clears the display 266 | (0x0, 0x0, 0xE, 0x0) => { 267 | self.display = [[chip8_base::Pixel::default(); 64]; 32]; 268 | return Some(self.display); 269 | } 270 | _ => panic!("Instruction either doesn't exist or hasn't been implemented yet"), 271 | }; 272 | None 273 | } 274 | ... 275 | 276 | impl chip8_base::Interpreter for ChipState { 277 | fn step(&mut self, keys: &chip8_base::Keys) -> Option { 278 | let instr = self.fetch(); 279 | self.execute(instr) 280 | } 281 | ... 282 | ``` 283 | 284 | Note the return is needed to pass the display back since clear updates the display, also pattern matching on hex to match e.g. `0xE` and stay consistent. 285 | 286 | ## Task 3.2 287 | 288 | ```rust,noplayground 289 | fn nnn(instruction: u16) -> u16 { 290 | instruction & 0x0FFF 291 | } 292 | ... 293 | 294 | fn execute(&mut self, instruction: u16) -> Option { 295 | ... 296 | // 1nnn JP addr: Jump to location nnn 297 | (0x1, _, _, _) => self.program_counter = Self::nnn(instruction), 298 | ... 299 | } 300 | ``` 301 | 302 | Here we use a bitmask to chop off the first bit to get the last 12. This approach disregards the last 3 nibbles in the pattern match, since those variables aren't used, and are taken straight from `instruction` instead. You could also construct `nnn` from those nibbles, though it is more involved. 303 | 304 | ## Task 3.3 305 | 306 | ```rust,noplayground 307 | fn kk(instruction: u16) -> u8 { 308 | (instruction & 0x00FF) as u8 309 | } 310 | ... 311 | 312 | fn execute(&mut self, instruction: u16) -> Option { 313 | ... 314 | // 6xkk LD Vx, byte: Set Vx = kk. 315 | (0x6, x, _, _) => self.registers[x as usize] = Self::kk(instruction), 316 | ... 317 | ``` 318 | 319 | Nearly identical to above, but using `kk` to match the last byte instead of 12 bits. 320 | 321 | ## Task 3.4 322 | 323 | ```rust,noplayground 324 | fn execute(&mut self, instruction: u16) -> Option { 325 | ... 326 | // 7xkk ADD Vx, byte: Set Vx = Vx + kk. 327 | (0x7, x, _, _) => { 328 | self.registers[x as usize] = self.registers[x as usize].wrapping_add(Self::kk(instruction)); 329 | } 330 | ... 331 | ``` 332 | 333 | As the hint gave, `wrapping_add` wraps around the overflow as required. 334 | 335 | ## Task 3.5 336 | 337 | Add `index: u16` to the struct and `new`. 338 | 339 | ```rust,noplayground 340 | fn execute(&mut self, instruction: u16) -> Option { 341 | ... 342 | // Annn LD I, addr: Set I = nnn. 343 | (0xA,_,_,_) => self.index = Self::nnn(instruction), 344 | ... 345 | ``` 346 | 347 | ## Task 3.6 348 | 349 | ```rust,noplayground 350 | fn execute(&mut self, instruction: u16) -> Option { 351 | ... 352 | // Dxyn DRW Vx, Vy, nibble: Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision. 353 | (0xD, x, y, n) => { 354 | let tlx = self.registers[x as usize] % 64; 355 | let tly = self.registers[y as usize] % 32; 356 | self.registers[0xF] = 0; 357 | let ind = self.index as usize; 358 | let sprite = &self.memory[ind..(ind + n as usize)]; 359 | 360 | for (i, row) in sprite.iter().enumerate() { 361 | let pxy = tly + i as u8; 362 | if pxy > 31 { 363 | break; 364 | } 365 | 366 | for j in 0..8 { 367 | let pxx = tlx + j; 368 | if pxx > 63 { 369 | break; 370 | } 371 | let old_px = &mut self.display[pxy as usize][pxx as usize]; 372 | let mask = 2_u8.pow(7 - j as u32); 373 | let new_u8 = (row & mask) >> (7 - j); 374 | let new_px: chip8_base::Pixel = new_u8.try_into().unwrap(); 375 | if (new_px & *old_px).into() { // if collision 376 | self.registers[0xF] = 1 377 | } 378 | *old_px ^= new_px; 379 | } 380 | } 381 | return Some(self.display) 382 | }, 383 | ... 384 | ``` 385 | 386 | This is a translation of the rough pseudocode into Rust. Note how iterating over bits is a bit of a pain. However, iterating over the sprite super is easy: we just grab it as a slice. Remember slices? If not, check [The Book](https://doc.rust-lang.org/book/ch04-03-slices.html) 387 | 388 | ## Task 3.7 389 | 390 | Here is a load function to load a ROM into memory from disk: 391 | 392 | ```rust,noplayground 393 | pub fn load(mut self, filename: &str) -> std::io::Result { 394 | let program = std::fs::read(filename)?; 395 | self.memory[0x200..(0x200 + program.len())].copy_from_slice(&program); 396 | Ok(self) 397 | } 398 | ``` 399 | 400 | Note how this takes ownership, and then returns `std::io::Result`. We return `Err` if we have some error reading from disk, and the error is returned early to the caller using [`the ? operator](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html), which we'll cover in more detail next time. Loading a ROM copies the bytes into memory and then moves the PC to the start of the program. Finally, we give ownership back to the caller if everything is okay. 401 | 402 | You could also do this capturing `self` by mutable reference, or handle the I/O error here instead of bubbling it up to the caller. All up to you. 403 | 404 | ## Task 4 405 | 406 | You really are on your own here. 407 | 408 | Try to ask for help, check your resources, and debug properly first before going straight to the nuclear option of just copying it from my solution, but you can find a full implementation (approximately) following on from this one at [ericthelemur/chip8](https://github.com/ericthelemur/chip8), and one that separates decode and execute at [`rs118-chip8`](https://github.com/uwcs/rs118-chip8). 409 | 410 | Note that these solutions are certainly not infallible, so don't rely on it as a source of truth for CHIP-8 implementations! 411 | -------------------------------------------------------------------------------- /src/img/rt/fig-antialias.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.11-reflection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.14-cam-view-geom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/1.md: -------------------------------------------------------------------------------- 1 | # Part 1: Hello, Ferris! 2 | 3 | ## Installing Rust ([1.1](https://doc.rust-lang.org/book/ch01-01-installation.html)) 4 | 5 | We have a few bits we'll need 6 | 7 | - [`rustup`](https://rustup.rs/) for managing versions of Rust and the other tools below 8 | - [`cargo`](https://doc.rust-lang.org/cargo/) Rust's build tool and package manager. 99% of Rust projects use cargo. 9 | - [`rustc`](https://doc.rust-lang.org/rustc/what-is-rustc.html) the Rust compiler itself 10 | - [`rust-analyzer`](https://rust-analyzer.github.io/) the Rust language server 11 | 12 | This is made easy with `rustup`, which can be installed by running the command below. If you're on a system other than DCS, go to for installation instructions. 13 | 14 | ```bash 15 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 16 | ``` 17 | 18 | Follow the on-screen instructions, and this will install Rust's default stable toolchain to `$HOME/.cargo`. `cargo` can use a large amount of space, so if you are concerned about filling up your home directory (e.g. filling your disk quota), you can set `RUSTUP_HOME` and `CARGO_HOME` to somewhere else, e.g. `/var/tmp/rustup` (this will not persist, but it won't fill your quota). 19 | 20 | You'll need to install `rust-analyzer` separately. If you're using [VS Code](https://code.visualstudio.com/), the command 21 | 22 | ```bash 23 | code --install-extension rust-lang.rust-analyzer 24 | ``` 25 | 26 | will install `rust-analyzer` for you. See [the `rust-analyzer` user manual](https://rust-analyzer.github.io/manual.html) for instructions for other editors. 27 | 28 | ## Hello World ([1.2](https://doc.rust-lang.org/book/ch01-02-hello-world.html) & [1.3](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html)) 29 | 30 | Before we write any code, we need a new Cargo project, or "crate". 31 | 32 | ```bash 33 | cargo new hello_world 34 | ``` 35 | 36 | Open your new Cargo project, and in the `hello_world` folder, you should see: 37 | 38 | - `Cargo.toml`, Cargo's config file, in [TOML](https://toml.io/en/) (Tom’s Obvious, Minimal Language) format. 39 | - `src/main.rs` 40 | - The `src` directory is where all source code should live 41 | - `main.rs` is the top-level source file in the crate. It's where the `main()` function should live. 42 | - A `.git` folder - Cargo automatically `init`s a git repo for you. 43 | - It also adds a default `.gitignore` 44 | 45 | Cargo has multiple commands that facilitate the building and running of Rust projects 46 | 47 | - `cargo run` will build and run your code, using `main::main` as the entry point 48 | - `cargo build` will just compile the crate 49 | - `cargo check` will check your crate for errors 50 | - `cargo fmt` will format your code using [rustfmt](https://github.com/rust-lang/rustfmt) 51 | - `cargo clippy` will lint your code using [clippy](https://github.com/rust-lang/rust-clippy) 52 | 53 | See [The Book](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html), and also [The Cargo Book](https://doc.rust-lang.org/cargo/index.html), for more info about Cargo and the `Cargo.toml` file. 54 | 55 | Open `main.rs` in VS Code and, oh look, you don't even need to write hello world: Cargo did it for you. You can delete it and write it out again, if you really want. 56 | 57 | ```rust, editable 58 | fn main() { 59 | println!("hello world"); 60 | } 61 | ``` 62 | 63 | - The `fn` keyword is used to declare functions 64 | - `main` is the name of the function 65 | - Parentheses `()` is where the parameter list goes, and braces `{}` are used to declare blocks 66 | - The `println!()` macro is used for command line output (more on macros/functions later) 67 | 68 | ## Variables ([3.1](https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html)) 69 | 70 | Variables in Rust are declared, or _bound_, using the `let` keyword: 71 | 72 | ```rust 73 | let x = 6; 74 | println!("The value of x is: {}", x); 75 | ``` 76 | 77 | Variables are **immutable by default**, meaning their value _cannot be changed_ once they have been bound. This is _A Good Thing™_ because immutability means less potential for errors. Prefer to leave variables as immutable, unless you absolutely have to, in which case mutable variables can be declared `mut`: 78 | 79 | ```rust, editable 80 | fn main() { 81 | let mut x = 5; 82 | println!("The value of x is: {}", x); 83 | x = 6; 84 | println!("The value of x is: {}", x); 85 | } 86 | ``` 87 | 88 | We'll talk about type annotations and inference later. 89 | 90 | ## Types 91 | 92 | ### Basic Types ([3.2](https://doc.rust-lang.org/book/ch03-02-data-types.html)) 93 | 94 | Rust has the following numeric types: 95 | 96 | | Bit Width | Signed | Unsigned | 97 | | -------------------- | ------- | -------- | 98 | | 8 | `i8` | `u8` | 99 | | 16 | `i16` | `u16` | 100 | | 32 | `i32` | `u32` | 101 | | 64 | `i64` | `u64` | 102 | | 128 | `i128` | `u128` | 103 | | architecture-defined | `isize` | `usize` | 104 | 105 | (`usize/isize` are the pointer width of the architecture you're compiling for. You should use them for anything representing size/length/indices of data structures, such as arrays.) 106 | 107 | We also have floats, `f32` and `f64` which are IEEE754 floating point types (equivalent to `float` and `double` in many languages). 108 | 109 | Booleans are of type `bool` and are either `true` or `false`. 110 | 111 | Characters are of type `char` and are written using single quotes: 112 | 113 | ```rust 114 | let c = 'c'; 115 | let z = 'Z'; 116 | let heart_eyed_cat = '😻'; 117 | ``` 118 | 119 | `char`s in Rust are four bytes in size, as a char is meant to represent a single Unicode scalar value, meaning it can do much more than just ASCII. 120 | 121 | ### Primitive Compound Types ([3.2](https://doc.rust-lang.org/book/ch03-02-data-types.html)) 122 | 123 | Rust has tuples, which are used extensively. Tuples can be non-homogenous and of any length. 124 | 125 | ```rust 126 | let tup = (1, 2); 127 | let trip: (char, i32, f64) = ('A', -2, 100.01); 128 | ``` 129 | 130 | Tuples are accessed using dot syntax, or destructured by pattern matching (more on this later): 131 | 132 | ```rust 133 | # let tup = (1, 2); 134 | # let trip: (char, i32, f64) = ('A', -2, 100.01); 135 | let one = tup.0 + tup.1 + trip.1; 136 | let (x, y) = tup; 137 | let (c, i, _) = trip; 138 | ``` 139 | 140 | Note the `_` binding, which is used in pattern matching to discard a value: 141 | 142 | ```rust, editable 143 | fn main() { 144 | let _ = 1; 145 | print!("Can you use _? {}", _); 146 | } 147 | ``` 148 | 149 | Arrays in Rust have a fixed length and are allocated on the stack. They are indexed using brackets `[]`, and have a type signature `[type; length]`. The length of an array is part of it's type in Rust. 150 | 151 | ```rust, editable 152 | fn main() { 153 | let a = [1, 2 ,3]; 154 | let first = a[0]; 155 | let second = a[1]; 156 | 157 | let b: [f32; 2] = [-2.0, 4.1]; 158 | 159 | // you can also use shorthand for creating arrays of the same element! 160 | let zeroes = [0; 10]; // ten zeroes! 161 | } 162 | ``` 163 | 164 | Attempting to index out of bounds will cause a **panic**. Panics are Rust's way of throwing unrecoverable errors at runtime, similar to exceptions in other languages. More on those later. 165 | 166 | ### Type Annotations 167 | 168 | `let` bindings can be annotated with their types using the following syntax: 169 | 170 | ```rust, editable 171 | fn main() { 172 | let a: u16 = 4; 173 | let b: i32 = -1; 174 | let c: char = 'c'; 175 | let d: &str = "hello!"; // More on string types shortly 176 | let e: (i32, f32) = (12, 12.0); 177 | let f: [u32; 3] = [1, 2, 3]; 178 | } 179 | ``` 180 | 181 | This is rarely necessary, as Rust can infer types most of the time. You can also annotate numeric literals with their types: 182 | 183 | ```rust 184 | let a = 4_u16; 185 | let b = -1_i32; 186 | let c = 3.14_f64; 187 | ``` 188 | 189 | ### Composite Data Types 190 | 191 | We can use these basic types to build more complex types. Rust has two ways of doing this. 192 | 193 | #### Structures ([5](https://doc.rust-lang.org/book/ch05-00-structs.html)) 194 | 195 | Those of you familiar with C will be familiar with `struct`s. Structs are types made up of several fields, where each field has a name and type. 196 | 197 | - Structs can be created by giving values to the types, using the syntax shown. 198 | - Fields can be accessed or updated using dot notation. 199 | - If a struct is bound as immutable, its fields are also immutable. 200 | 201 | ```rust, editable 202 | struct Student { 203 | name: String, 204 | id: String, 205 | year: u32, 206 | active: bool, 207 | } 208 | 209 | fn main() { 210 | let mut you = Student { 211 | name: String::from("Your Name"), 212 | id: String::from("1234567"), 213 | year: 2, 214 | active: false, 215 | }; 216 | println!("My student ID is: {}", you.id); 217 | you.year += 1; 218 | println!("My year of study is: {}", you.year); 219 | } 220 | ``` 221 | 222 | #### Enums ([6.1](https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html)) 223 | 224 | Enums restrict variables to a predefined finite set of named discrete values. It's short for enumeration, because it is enumerates of all the allowed options. Many languages have support for enums (e.g. C, Java, Haskell), but Rust's enums are most similar to Java's enums or sum types in functional languages such has Haskell and F#. 225 | 226 | Say we want to represent some colours. We have a Thing, and we need it to be either red, green, or blue: 227 | 228 | ```rust, editable 229 | enum Colour { 230 | Red, 231 | Green, 232 | Blue, 233 | } 234 | 235 | fn main() { 236 | let thing_colour: Colour = Colour::Red; 237 | } 238 | ``` 239 | 240 | We now have a new type, `Colour`, and three new values: `Colour::Red`, `Colour::Blue`, and `Colour::Green`. 241 | 242 | What makes enums really special is that variants can contain values: 243 | 244 | ```rust 245 | enum MyEnum { 246 | NoValue, 247 | ANumber(u32), 248 | TwoNumbers(u32, i64), 249 | AString(String), 250 | } 251 | ``` 252 | 253 | This is a really powerful feature, especially when used in combination with pattern matching, and part of what makes Rust's type system so good. 254 | 255 | An example, using an enum to represent some shapes: 256 | 257 | ```rust, editable 258 | enum Shape { 259 | Circle(u64), 260 | Rectangle(u64, u64), 261 | Triangle(u64, u64, u64), 262 | } 263 | 264 | fn main() { 265 | let circle = Shape::Circle(6); 266 | let triangle = Shape::Triangle(1, 2, 3); 267 | } 268 | ``` 269 | 270 | Or a recursive definition of a binary tree[^treenote]: 271 | 272 | ```rust 273 | enum Tree { 274 | Leaf(i32), 275 | Branch(Box, Box), 276 | } 277 | ``` 278 | 279 | #### `Option` 280 | 281 | A commonly used enum is `Option`. Option is used to represent values that may or may not exist. Rust has no notion of `null` (no more `NullPointerException`s!), instead preferring to wrap other types in an `Option` when some similar notion of null is needed. This has advantages, as it forces you to explicitly deal with error cases, among other things. 282 | 283 | `T` is a generic type parameter, which we'll cover in more detail later. 284 | 285 | ```rust 286 | enum Option { 287 | Some(T), 288 | None, 289 | } 290 | ``` 291 | 292 | Think of it as a type-safe container which may or may not be empty. This enum is frequently used as a return type in methods that may fail: 293 | 294 | ```rust, editable 295 | fn div(x: i64, y: i64) -> Option { 296 | if y == 0 { 297 | None 298 | } else { 299 | Some(x/y) 300 | } 301 | } 302 | 303 | fn main() { 304 | println!("{:?}", div(15, 3)); 305 | } 306 | ``` 307 | 308 | This particular enum is very important as it's used widely throughout the language, and we'll get to it in more detail later. 309 | 310 | [^treenote]: The actual use of the tree here is more complex, `Box`es are smart pointers with heap allocation (for unknown size data). More in the [Raytracer Project](raytracer.md#task-54), so don't worry about these for now. 311 | 312 | 313 | ## Functions ([3.3](https://doc.rust-lang.org/book/ch03-03-how-functions-work.html)) 314 | 315 | Functions in Rust are declared using the `fn` keyword. 316 | 317 | - The list of parameters is declared between the parentheses: `name: type` 318 | - The return type is given using an arrow `-> type` 319 | - Functions with no return type implicitly return `()`, [the unit type](https://doc.rust-lang.org/std/primitive.unit.html). 320 | 321 | ```rust, editable 322 | fn main() { 323 | println!("this function has no parameters and no return type"); 324 | } 325 | 326 | fn inc(x: i32) -> i32 { 327 | println!("this function returns its parameter plus one"); 328 | return x+1; 329 | } 330 | 331 | fn mul(a: f64, b: f64) -> f64 { 332 | println!("this function returns the product of its parameters"); 333 | a*b 334 | } 335 | 336 | fn zip(fst: i32, snd: i32) -> (i32, i32) { 337 | (fst, snd) 338 | } 339 | ``` 340 | 341 | Note how in the last examples the `return` keyword was not used and there is no semicolon. This is because in Rust, (almost) everything is an expression that returns a value, even blocks. The value of the last expression in a block is the return value of the block. Ending an expression with a semicolon discards the return value. This can be a tricky concept to grasp, see [Rust by Example](https://doc.rust-lang.org/rust-by-example/expression.html) for more detail. 342 | 343 | ## Control Flow ([3.5](https://doc.rust-lang.org/book/ch03-05-control-flow.html)) 344 | 345 | ### `if` Expressions 346 | 347 | You all hopefully know how these work. The syntax is shown below. In Rust, you don't need parentheses around the condition. 348 | 349 | ```rust, editable 350 | fn main() { 351 | let number = 6; 352 | 353 | if number % 4 == 0 { 354 | println!("number is divisible by 4"); 355 | } else if number % 3 == 0 { 356 | println!("number is divisible by 3"); 357 | } else if number % 2 == 0 { 358 | println!("number is divisible by 2"); 359 | } else { 360 | println!("number is not divisible by 4, 3, or 2"); 361 | } 362 | } 363 | ``` 364 | 365 | Boolean expressions are combined with 366 | 367 | - `&&` and 368 | - `||` or 369 | - `!` not 370 | - `&&` and `||` are [lazily evaluated](https://doc.rust-lang.org/reference/expressions/operator-expr.html#lazy-boolean-operators) 371 | 372 | `if` expressions return a value too, so it can be used as a ternary conditional operator: 373 | 374 | ```rust 375 | let condition = true; 376 | let x = if condition { 5 } else { 6 }; 377 | ``` 378 | 379 | ### `loop` Loops 380 | 381 | `loop` expressions repeat ad infinitum. 382 | 383 | ```rust, noplayground 384 | loop { 385 | println!("again!"); 386 | } 387 | ``` 388 | 389 | You can `break` out of one, if needs be. This is preferred to `while true`. You can also use one to construct a do-while: 390 | 391 | ```rust, noplayground 392 | loop { 393 | do_thing(); 394 | if !condition { 395 | break; 396 | } 397 | } 398 | ``` 399 | 400 | ### `while` Loops 401 | 402 | Pretty standard stuff too: 403 | 404 | ```rust 405 | let mut number = 3; 406 | 407 | while number != 0 { 408 | println!("{}", number); 409 | 410 | number -= 1; 411 | } 412 | ``` 413 | 414 | ### `for` Loops 415 | 416 | `for` loops are more like Python's, or Java's range-based for loop, than C/C++. They are used to iterate over a collection, using an _iterator_ (more on those later). 417 | 418 | ```rust 419 | let a = [10, 20, 30, 40, 50]; 420 | 421 | for element in a { 422 | println!("the value is: {}", element); 423 | } 424 | ``` 425 | 426 | If you want to loop over a numerical range, you can create a range iterator. Like Python's range, this is exclusive of the last number: 427 | 428 | ```rust 429 | for number in 1..10 { 430 | println!("{}", number); 431 | } 432 | 433 | for number in (1..10).rev() { 434 | // this loop goes from 10 -> 1 435 | println!("{}", number); 436 | } 437 | ``` 438 | 439 | ## Strings 440 | 441 | Strings in Rust are [not so simple](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqolzk0s0umg647d7ogg.jpeg). For now, we will consider only `String`. `String` represents a mutable, variable length buffer which can hold your `char`s. Create an empty one with `String::new()`, or create one from a literal using [`String::from()`](https://doc.rust-lang.org/std/string/struct.String.html#method.from-3), [`.to_owned()`](https://doc.rust-lang.org/std/primitive.str.html#method.to_owned) or [the trait `into`](https://doc.rust-lang.org/std/convert/trait.Into.html). 442 | 443 | ```rust, editable 444 | fn main() { 445 | let empty_string = String::new(); 446 | let hello_world = String::from("Hello, World!"); 447 | let hello_ferris: &str = "Hello, Ferris"; 448 | let owned_ferris = hello_ferris.to_owned(); 449 | } 450 | ``` 451 | 452 | The `::` symbol is used to access associated functions which belong to a type. The `from()` and `new()` functions both belong to `String`, so we call them as shown. Think of them as static methods, for those of you into your Java. 453 | 454 | ## Pattern Matching ([6.2](https://doc.rust-lang.org/book/ch06-02-match.html)) 455 | 456 | Those of you familiar with functional languages will be pleased to hear that Rust can do this. Those of you not lucky enough to be familiar with functional languages will likely be a bit confused. Think of it as a fancy switch statement for now. Here's an example, using our `Colour` enum from earlier: 457 | 458 | ```rust, editable 459 | enum Colour { 460 | Red, Blue, Green, White, Black 461 | } 462 | fn colour_to_rgb(colour: Colour) -> (u8, u8, u8) { 463 | match colour { 464 | Colour::Red => (255, 0, 0), 465 | Colour::Green => (0, 255, 0), 466 | Colour::Blue => (0, 0, 255), 467 | Colour::White => (255, 255, 255), 468 | Colour::Black => (0, 0, 0), 469 | } 470 | } 471 | 472 | fn main() { 473 | colour_to_rgb(Colour::Blue); 474 | } 475 | ``` 476 | 477 | The cases come before the `=>`, and the return value for that **match arm** comes after it. The match returns one of those tuples, and then the function returns the return value of the `match`. Remember that we don't need a `return` 478 | 479 | However, matching is much more powerful than this. We can include arbitrary expressions after the match arm: 480 | 481 | ```rust, editable 482 | # enum Colour { 483 | # Red, Blue, Green, White, Black 484 | # } 485 | fn colour_to_rgb(colour: Colour) -> (u8, u8, u8) { 486 | match colour { 487 | Colour::Red => (255, 0, 0), 488 | Colour::Green => (0, 255, 0), 489 | Colour::Blue => { 490 | println!("I'm blue, da ba dee da ba di"); 491 | (0, 0, 255) 492 | }, 493 | Colour::White => if 10 % 2 == 0 { 494 | (255, 255, 255) 495 | } else { 496 | unreachable!("Something is very wrong"); 497 | }, 498 | Colour::Black => (0, 0, 0), 499 | } 500 | } 501 | 502 | fn main() { 503 | let rgb = colour_to_rgb(Colour::Blue); 504 | println!("{:?}", rgb); 505 | } 506 | ``` 507 | 508 | We can also use it to destructure (unpack) and bind values, for use in the `match` body. For example, with `Option`: 509 | 510 | ```rust, editable 511 | fn maybe_increment(x: Option) -> Option { 512 | match x { 513 | None => None, 514 | Some(i) => Some(i + 1), 515 | } 516 | } 517 | fn main() { 518 | let five = Some(5); 519 | let six = maybe_increment(five); 520 | let none = maybe_increment(None); 521 | } 522 | ``` 523 | 524 | Note that matches must be exhaustive. For example, if you're matching on a `u8`, you must cover all cases from 0 to 255. This is impractical, so the placeholder `_` can be used, as a default case: 525 | 526 | ```rust, editable 527 | fn main() { 528 | let val: u8 = 40; 529 | match val { 530 | 12 => println!("val is 12"), 531 | 21 => println!("val is 21"), 532 | _ => println!("val is some other number that we don't care about"), 533 | } 534 | } 535 | ``` 536 | 537 | Pattern matching can also make use of further conditions (called _guards_). These require a fallback case to act as an else, even if it is only a `_`. There are other places pattern matching can be used, such as in [`if let`](https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html) and [`while let`](https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html) expressions. 538 | 539 | ```rust, editable 540 | fn maybe_collatz(x: Option) -> Option { 541 | match x { 542 | None => None, 543 | Some(1) => None, 544 | Some(i) if i % 2 == 0 => Some(i / 2), 545 | Some(i) => Some(3 * i + 1) 546 | } 547 | } 548 | fn main() { 549 | let mut number = Some(7_u64); 550 | while let Some(i) = number { 551 | println!("{i}"); 552 | number = maybe_collatz(number); 553 | } 554 | } 555 | ``` 556 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.09-rand-vec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/chip8.md: -------------------------------------------------------------------------------- 1 | # CHIP-8 Project 2 | 3 | [CHIP-8](https://en.wikipedia.org/wiki/CHIP-8) is an interpreted programming language, developed by Joseph Weisbecker. It was initially used on the COSMAC VIP and Telmac 1800 8-bit microcomputers in the mid-1970s. CHIP-8 programs are run on a CHIP-8 virtual machine. It was made to allow video games to be more easily programmed for these computers, but CHIP 8 is still used today, due to its simplicity, and consequently on any platform and its teaching of programming binary numbers. 4 | 5 | In short, its a really basic assembly-language-type specification that lets people build neat games, and we're going to build an interpreter for it, applying some of the Rust we've learned so far. 6 | 7 | Our finished interpreter is available on [crates.io](https://crates.io/crates/rs118-chip8), so you can install it with `cargo install rs118-chip8` to have a play with it, so you can see what your final product should look like. Plenty of ROMs are available online, we recommend [Space Invaders](https://github.com/UWCS/rs118-chip8/blob/main/roms/Space%20Invaders.ch8) and [Tetris](https://github.com/UWCS/rs118-chip8/blob/main/roms/Tetris.ch8). 8 | 9 | Also, if you haven't read [chapter 7](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html) of The Book, I'd recommend doing so. Knowing how to lay out a Rust program is something that's often overlooked but super important. 10 | 11 | ## 0: Getting Started 12 | 13 | ### Task 0.1: Read the Docs 14 | 15 | Before you do anything, have a read of [the CHIP-8 specification](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM), so you have a rough idea of what it is you need to implement and can start thinking about a solution. Think about how you'll represent the different components in Rust. 16 | 17 | ### Task 0.2: Read More Docs 18 | 19 | Create a new cargo project, and open up the `Cargo.toml` file. Our emulator exposes some stuff as a library for you to base your solution around, so add that to your dependencies: 20 | 21 | ```toml 22 | [dependencies] 23 | chip8_base = "0.2" 24 | ``` 25 | 26 | Take a look at the [`chip8_base`](https://docs.rs/chip8_base/latest/chip8_base/) library, that we'll be using as the base for our implementation. We can see that it exposes two type aliases, `Display` and `Keys`, a `Pixel` enum, and an `Intepreter` trait. The idea is that you create your own interpreter by implementing the trait, and then the `run()` method will run it for you. This works because `run()` is written to accept any type that implements the `Interpreter` trait as an argument. 27 | 28 | There's 3 functions our own interpreter will need to provide for a complete trait implementation: 29 | 30 | - `step(&mut self, keys: &Keys) -> Option` 31 | - `speed(&self) -> Duration` 32 | - `buzzer_active(&self) -> bool` 33 | 34 | The first one is the main driver function of your interpreter that you'll have to implement, and will execute it one cycle at a time, modifying internal state as necessary. The other two are just helpers to provide the trait with some more info it needs to run your interpreter correctly. Have a look at those function signatures and what the types tell you about what you might need to do. 35 | 36 | ### Task 0.3: Git Good 37 | 38 | Cargo initialises your new project as a git repo. It does this for a reason, to encourage you to use version control. If you aren't familiar with git, check out our [Git Good resources](https://uwcs.co.uk/resources/git-good-2022/). Make a new commit every time you change or try something, so you can keep track of what you've done and roll back if things break. Commit _at least_ when you've done each task. 39 | 40 | ## 1: The Virtual Machine 41 | 42 | The first step we're gonna take is to lay out our program using modules. Refer back to The Book if you need a refresher on how to create modules and structure a Rust program. 43 | 44 | ### Task 1.1: Modules 45 | 46 | Create a new directory next to `main.rs` called `interpreter`, and then add `interpreter/mod.rs`. Add the line `mod interpreter;` to the top of `main.rs` so the `interpreter` module is added to the module tree. This module is where most of our code is going to live. Feel free to create any other modules you wish alongside `mod.rs` too, but don't forget to include them in the module tree. 47 | 48 | ### Task 1.2: The Interpreter Type 49 | 50 | In our new interpreter module, we want to create a struct that will represent the state of our CHIP-8 virtual machine. 51 | 52 | Create a new struct type, adding fields from [the spec](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#2.0): memory, registers, display (following [`chip8_base::Display`](https://docs.rs/rs118-chip8/latest/chip8_base/type.Display.html)). Also add an `impl` with a `new()` associated function to return a default copy of our struct. `new()` can take whatever arguments you see fit to create a new virtual machine, but for now, none is fine. 53 | 54 | ### Task 1.3: The Interpreter Trait 55 | 56 | Before we can use the `run()` function from the `chip8_base` module, we need to tell the compiler that our interpreter is actually an interpreter, by implementing the `Interpreter` trait on it. 57 | 58 | Import the `chip8_base::Interpreter` trait into your interpreter module, and use another `impl` block to create the skeleton of your implementation of the trait on your struct. 59 | 60 | ### Task 1.4: Keep the Compiler Happy 61 | 62 | The compiler should be screaming at you right about now, because your type is implementing a trait without providing definitions for any of its methods. Go ahead and add the three required methods to your `impl` block, marking them as [`todo!()`](https://doc.rust-lang.org/std/macro.todo.html) for now. 63 | 64 | Note: `rust-analyzer` can do this for you if you ask it nicely (in VS Code, press `Ctrl` + `.` inside the `impl` and select `Implement missing members`). 65 | 66 | ### Task 1.5: One Step at a Time 67 | 68 | So, now you have your own skeleton of an interpreter, you can call `step()` on it to step through execution one clock cycle at a time. We do however need somewhere to create and run things from: `main()`. 69 | 70 | Head back to `main.rs` and use your `new()` function from the `interpreter` module to create a new virtual machine (check your `pub`s). Then, call `chip8_base::run()`, passing the struct you just instantiated as an argument. Have a look at the type signature for `chip8_base::run()` and think about how exactly to pass your arguments, considering ownership. 71 | 72 | This should all compile now, so `cargo run` it and see what happens! 73 | 74 | ### Task 1.6: Now I'm Panicking 75 | 76 | Well, you left the methods marked as `todo!()`, so it panicked. That was silly. We can provide some really barebones implementations though that don't do anything. Look at the return type for the three methods and think about what you might want to return. 77 | 78 | Make the three `Interpreter` methods return something such that they don't really do anything, but still don't panic. 79 | 80 | ### Task 1.7: Timing 81 | 82 | Have another look at the `speed()` method. To run games properly, the interpreter needs to run at a fixed clock speed. The return type of `speed()` is [`Duration`](https://doc.rust-lang.org/stable/std/time/struct.Duration.html), which is Rust's representation of a period of time. You can create `Duration` types with a number of associated functions, take a look at the docs to see which one is appropriate here. Your interpreter type should store some representation of the speed it is currently being run at (a clock frequency of 700Hz is generally pretty good for most CHIP-8 games), and be able to return the speed as a `Duration` so that the `run()` function knows exactly how fast to run it. 83 | 84 | Make your `speed()` method return a period of time corresponding to the speed of the interpreter. You should not hard-code this, make it a configurable constant or something passed when instantiating a new interpreter, as different games may require different speeds. 85 | 86 | ## 2: Fetch-Decode-Execute 87 | 88 | Your interpreter should be happily churning away burning CPU cycles for now, and the display window should work, letting you drag it around, resize it, etc. So, we have a virtual machine that does nothing. That's not very exciting at all, so let's change that. The virtual machine works pretty much the same as a CPU, with a fetch-decode-execute cycle. This entire cycle is one step, and what should be implemented in your step function. The basic idea of `Interpreter::step()` is: 89 | 90 | - Get the next instruction from memory (fetch) 91 | - Increment the program counter 92 | - Work out what that instruction needs to do (decode) 93 | - Execute the instruction (execute) 94 | 95 | ### Task 2.1: Fetch 96 | 97 | Let's start with fetch. Looking at the spec, we can see that each CHIP-8 opcode is two bytes, composed of four nibbles (each nibble being four bits, or one hex digit). The program counter is 16 bits, and should point to the address of the next instruction to be executed from CHIP-8's 4kB of memory. 98 | 99 | Write a function, `fetch`, to return the next opcode to be executed from memory, and increment the program counter. Consider carefully the parameters and return types of your method, how do ownership rules interact with it? Add a call to `fetch` within `step`. If you haven't already got fields for memory and program counter on your struct, now is the time to add them. 100 | 101 | ### Task 2.2: Fetch, But it Works 102 | 103 | So when you run your program now, what should be happening is opcodes are continually fetched from memory, until... it panics? Yes, panics. What's happening is your program is continually fetching until the program counter overflows, which causes a panic when Rust is in debug mode ([see this blog post for a good rundown on overflow in Rust](https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/)). To fix this, we need to manually specify a way to make our program counter wrap around back to 0. The program counter is meant to represent a 12-bit address (for CHIP-8's 4096 bytes of memory), so we should wrap back to 0 when it reaches 4096. 104 | 105 | Fix your fetch instruction to wrap back to 0 when it reaches the end of the addressable memory. Add in some debug statements (take a look at [the excellent `dbg!()` macro](https://doc.rust-lang.org/std/macro.dbg.html)) to verify that it is fetching continually from memory, returning 0 each time (we haven't loaded anything into memory yet). 106 | 107 | ### Task 2.3: Logging 108 | 109 | When writing code, logging events to the terminal can be really useful to help determine what is going on in your program. The [log crate](https://docs.rs/log/latest/log/) provides some macros used for logging events to the console, and good libraries will generally include logs to help users see what's going on. Libraries can choose to log events, but it's up to the user to initialise a logging implementation to consume the logs. We recommend the simple [`env_logger`](https://docs.rs/env_logger/latest/env_logger/), which let's you configure logging via an environment variable. See the examples on the docs pages for those two crates to get an idea of how they work. 110 | 111 | Add both `log` and `env_logger` to your package manifest, then add a call to `env_logger::init()` as the first call in your `main()` function. Run your code again, but prepend the cargo command with `RUST_LOG=info` (ie, `RUST_LOG=info cargo run ...`) and you should see the library, and other crates we depend on such as `wgpu`, doing things. Set the log level to debug to see even more. 112 | 113 | If you run `export RUST_LOG=info`, then that sets the log level for the entire terminal session. Error logs will always be printed by default, but you might find it useful to leave `warn` or `info` logs on. 114 | 115 | Add some logging calls using the `log` macros to your code so you can easily trace what's going on. Try to choose an appropriate log level for each call, to make your logs easy to consume and filter. Using `trace` and `debug` logs are useful for tracking down issues, you'll thank yourself later on. 116 | 117 | ### Task 2.4: Decode 118 | 119 | So, we can fetch instructions, but what do we do with them? Well, execute them of course. CHIP-8 has 35 instructions with a varying number of operands. We could write a very very long chain of if/else expressions to decode each instruction, or we could use one `match` expression. Each instruction has four nibbles, some of which are fixed to specify the opcode, some of which are operands of varying length (4, 8 or 12 bit). We can use this to write our `execute()` function (you could separate decode and execute, but it's easiest to combine them for simplicity). 120 | 121 | Write an `execute` method that uses a `match` expression to decode instructions. Just pattern match for now, no need to execute anything yet (that's spoilers for part 2.5). There are many ways you could go about this, but I recommend breaking each instruction down into it's four nibbles, then pattern matching on that. For now, make each of your match arms do nothing (return the unit type). Remember that `match` expressions in rust have to cover all possible types, so you can use a wildcard pattern (`_ => ()`) to cover any unimplemented instructions, or instructions that don't exist. **You don't have to do all of the instructions now,** just maybe have a look at the essentials for later (see section 3) and check the advice at the bottom for a little help. Refer back to [The Book](https://doc.rust-lang.org/book/ch06-02-match.html) (Chapter 18 may also be useful) if you need a refresher on how `match` works. 122 | 123 | ### Task 2.5: Execute 124 | 125 | So we've done fetch and decode, no prizes as to what comes next. Executing an instruction just consists of modifying the internal state of your virtual machine accordingly, so double-check [the specification](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM) at this point to make sure you have all the fields you need on your interpreter struct. 126 | 127 | First implement the opcode `0x000` as a NOP (No-Operation) instruction, that simply does nothing. Then fill in a few of the arms of your match statement to execute the decoded instructions (perhaps take some inspiration from section 3). Think about how you're going to get the operands out of the instruction when they vary in width. Make your `step()` method call `execute()` so that the interpreter is now doing the fetch-decode-execute cycle. 128 | 129 | Congrats! You're successfully emulating a CPU* (*CHIP-8 is not a CPU but it's an awful lot like one). Take a moment to appreciate how cool this is, even if it does nothing so far. What should be happening is the interpreter is fetching, decoding and executing `0x0000` instructions continually, which aren't real instructions but I added them because I could. 130 | 131 | ## 3: The First Few Instructions 132 | 133 | In the last section, you implemented one or two random instructions of your choosing, plus our fictitious NOP instruction. Now, I'm gonna talk you through implementing a few more, such that you can run a real program. We've written a UWCS test ROM that does nothing except display the UWCS logo, but using only 6 instructions, so those are what we'll implement. 134 | 135 | ### Task 3.1: 00E0 (clear screen) 136 | 137 | This instruction should set all the display's pixels to black. If you don't have some representation of the display in your struct, add one now (look at the [`Display`](https://docs.rs/rs118-chip8/latest/chip8_base/type.Display.html) type if you need a hint). 138 | 139 | The `step()` function should return `Some(Display)` when the display is updated, so maybe your execute function wants to do the same, and then the step function should return that? Either way, make sure that executing this instruction causes your updated display state to be returned to `step()`'s caller. 140 | 141 | ### Task 3.2: 1nnn (jump) 142 | 143 | This instruction has one operand, `nnn`, a 12-bit address that should be pushed onto the stack. Rust doesn't have a 12-bit number type, so pick another type accordingly and include checks/wrapping to make sure that the value remains within 12 bits. The program counter should simply be set to the value of the operand. 144 | 145 | ### Task 3.3: 6xkk (set register Vx) 146 | 147 | CHIP-8 has 16 general purpose 8-bit registers, numbered `V0` to `VF`. `VF` is often used for special flags from some operations. This instruction has two operands, `x`, the register, and `kk`, a byte. The byte should be put in the register. Easy. 148 | 149 | ### Task 3.4: 7xkk (add to register Vx) 150 | 151 | Add the value `kk` to the value in the register `Vx`. This instruction may overflow (causing a panic), so make sure to handle overflow/wrapping correctly. 152 | 153 | [**Hint**](https://doc.rust-lang.org/std/primitive.u8.html#method.wrapping_add) 154 | 155 | ### Task 3.5: Annn (set index register) 156 | 157 | The index register is a separate special 16-bit register, which is generally used to point at locations in memory. This instruction should set the index register to the 12-bit value `nnn`. 158 | 159 | ### Task 3.6: Dxyn (draw) 160 | 161 | This is certainly the most involved instruction in the whole CHIP-8 spec. CHIP-8 has sprites which are 8 bits wide and up to 15 bytes tall. This instruction draws the sprite starting at the address in the index register to the screen at position (`Vx`,`Vy`). Info on how sprites should wrap varies, but generally the X and Y coordinates should be modulo the display size, ie an X-coordinate of 69 should be interpreted as a coordinate of 6, and sprites should not partially wrap. Drawing works by XORing the pixel values with the current display values, and the `VF` register should also be set to `1` if this causes any pixels to be erased. 162 | 163 | - Set `X` to the value in `Vx` modulo 64 164 | - Set `Y` to the value in `Vy` modulo 32 165 | - Zero `VF` 166 | - For each row in the n-byte sprite 167 | - if y is out of bounds, stop drawing the sprite 168 | - For each **bit** in the byte/row 169 | - If x is out of bounds, stop drawing the row 170 | - XOR the bit onto the screen 171 | - Set `VF = 1` if this caused a pixel to be erased. 172 | 173 | The `Pixel` type has some traits implemented (mainly bitwise operator overloads and conversions) that you may find helpful, so check the docs page for that. Check the resources at the bottom (and also Google for anything else you can find) for more explanations, as implementations and explanations may vary ever so slightly, but the general idea is always the same. 174 | 175 | ### Task 3.7: This Tutorial Not Sponsored By UWCS 176 | 177 | Theoretically, you should be able to run the [UWCS test ROM](https://github.com/UWCS/rs118-chip8/blob/main/roms/uwcs.ch8) now. But first you need a way to load it into memory. CHIP-8 Programs start at `0x200` in memory, so you need to write a method to load a ROM from disk into memory and ensure your PC starts there. [`std::fs::read`](https://doc.rust-lang.org/stable/std/fs/fn.read.html) will load a file from disk and return it as `Vec` of bytes, but how to get it into memory is up to you. You could add it to your `new()` function, or create a separate `load()` function. Make sure you properly handle the `Result` that the `fs::read` returns too, in case you give it a file that doesn't exist. 178 | 179 | ### Task 3.8: Debugging 180 | 181 | Chances are this doesn't work first try (unless you're some next level god tier genius, in which case, congrats). You'll probably need to do some debugging. Making extensive use of the `dbg!()` macro and `debug` and `trace` logs is a good idea, or maybe slow down the speed of your emulator to make it easier to see what's going on step-by-step. Redirecting `stderr` to a file on the command line may come in handy too so, you can take a closer look. 182 | 183 | If you're using VS Code, debugging Rust is easy. `rust-analyzer` adds a little "debug" button above your main function you can press to launch the debugger, allowing you to step through and inspect values one step at a time. If you've never used a debugger in VS Code before, [have a look at this article](https://code.visualstudio.com/docs/editor/debugging). [This article includes some information about debugging Rust specifically.](https://dev.to/rogertorres/debugging-rust-with-vs-code-11dj). If you prefer the command line, `gdb` has support for rust too, through `rust-gdb`. 184 | 185 | Writing unit tests to test each instruction in isolation is a good idea too. [Chapter 11 of The Book](https://doc.rust-lang.org/book/ch11-00-testing.html) has some information on writing unit tests in rust, which is incredibly easy. Obviously you should always test your code, but a lot of the opcodes are fairly simple and we don't expect a full suite of unit tests for just a toy project. Writing a few to cover the more complex instructions and check your edge cases is a good idea, as you can then debug the execution of the tests in isolation too. 186 | 187 | ## 4: The Rest 188 | 189 | Well, we've got this far. However, you still have about 30 instructions before you can say you're done. A few test ROMS can be found [here](https://github.com/UWCS/rs118-chip8/tree/main/roms) for testing all your instructions work. Remember, unit tests are your friend, have a look at some of the unit tests in our implementation if you're stuck on how to write these. 190 | 191 | As a little bonus for getting this far, [here's a version of `main.rs`](https://github.com/ericthelemur/chip9/blob/master/src/main.rs) where you can pass the ROM and frequency as command line arguments. 192 | 193 | Some advice: 194 | 195 | - Make sure you implement the `buzzer_active()` function correctly. 196 | - The timer registers will be quite tricky to line the timing up correctly. You can rely on the fact that your `step()` function will be executed once every `t` seconds, where `t` is whatever `Duration` is returned by the `speed()` method. 197 | - Make sure you handle wrapping to 8/12/16 bit boundaries correctly, making use of the standard library's wrapping and saturating add/sub methods. 198 | - `n & 0xfff` will wrap `n` to a 12 bit boundary 199 | - Some instructions require you set `VF` under certain conditions. 200 | - This is a very specific use case where casting in Rust can be annoying, as CHIP-8 has no strong type system like Rust does. Make sure all your `as` and `into`/`from` casts are in the right place. 201 | - You don't have to completely re-architect the whole thing to implement the `Fx0A` instruction, trust me. Ask for help with this one if you need (how can you keep the PC from moving on?). 202 | - You'll need the `rand` crate from crates.io to generate random numbers 203 | - You'll need to initialise the font in memory at some point. Where/how is best to do this? Font usually starts at 0x50, but can be anywhere in the first 512 bytes. 204 | - Ask for help from a friend or lab tutor, or in [Discord](https://discord.uwcs.co.uk) if you get stuck 205 | - Look at existing implementations if you get really stuck 206 | 207 | Not all ROMS you find online will work, as some may be written for Super CHIP-8, an extension of CHIP-8 that adds a few more instructions. Feel free to extend your emulator with these instructions if you want. 208 | 209 | ## Resources 210 | 211 | The CHIP-8 Specification is available at 212 | 213 | A more detailed explanation of each instruction and more of the details of the "hardware" are available at 214 | 215 | A large collection of CHIP-8 stuff is available at 216 | 217 | A sample solution is available at or 218 | -------------------------------------------------------------------------------- /src/img/rt/fig-1.03-cam-geom-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | --------------------------------------------------------------------------------