├── tinlib
├── src
│ ├── machine
│ │ ├── input.rs
│ │ ├── code.rs
│ │ ├── memory.rs
│ │ ├── ram.rs
│ │ ├── vram.rs
│ │ ├── mod.rs
│ │ └── screen.rs
│ ├── common
│ │ ├── mod.rs
│ │ ├── size.rs
│ │ ├── error.rs
│ │ └── coord.rs
│ ├── graphic
│ │ ├── mod.rs
│ │ ├── color.rs
│ │ ├── palette.rs
│ │ ├── font.rs
│ │ └── glyph.rs
│ ├── lib.rs
│ ├── cartridge
│ │ ├── error.rs
│ │ ├── chunk.rs
│ │ └── mod.rs
│ └── map
│ │ └── mod.rs
├── README.md
├── Cargo.toml
└── examples
│ └── cartridge.rs
├── devkit
├── src
│ └── main.rs
├── README.md
└── Cargo.toml
├── player
├── README.md
├── Cargo.toml
└── src
│ └── main.rs
├── Cargo.toml
├── images
└── logo-128x128.png
├── .gitignore
├── CHANGELOG.md
├── .editorconfig
├── Makefile
├── .pre-commit-config.yaml
├── LICENSE
├── cliff.toml
├── .github
└── workflows
│ └── ci.yml
├── README.md
└── Cargo.lock
/tinlib/src/machine/input.rs:
--------------------------------------------------------------------------------
1 | #[derive(Default)]
2 | pub struct Input;
3 |
--------------------------------------------------------------------------------
/devkit/src/main.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | println!("Hello, world!");
3 | }
4 |
--------------------------------------------------------------------------------
/player/README.md:
--------------------------------------------------------------------------------
1 | # SN-50 Player
2 |
3 | The SN-50 Fantasy Computer player.
4 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["tinlib", "devkit", "player"]
3 | resolver = "2"
4 |
--------------------------------------------------------------------------------
/devkit/README.md:
--------------------------------------------------------------------------------
1 | # SN-50 DevKit
2 |
3 | The SN-50 Fantasy Computer with DevKit tools.
4 |
--------------------------------------------------------------------------------
/tinlib/README.md:
--------------------------------------------------------------------------------
1 | # tinlib
2 |
3 | Components for SN-50 Fantasy Computer implementations.
4 |
--------------------------------------------------------------------------------
/images/logo-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TinTeam/SN-50/HEAD/images/logo-128x128.png
--------------------------------------------------------------------------------
/tinlib/src/machine/code.rs:
--------------------------------------------------------------------------------
1 | pub struct Code {
2 | #[allow(dead_code)]
3 | chars: [char; 1],
4 | }
5 |
6 | impl Default for Code {
7 | fn default() -> Self {
8 | Self { chars: [' '; 1] }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # These are backup files generated by rustfmt
7 | **/*.rs.bk
8 |
9 | # MSVC Windows builds of rustc generate these, which store debugging information
10 | *.pdb
11 |
--------------------------------------------------------------------------------
/tinlib/src/common/mod.rs:
--------------------------------------------------------------------------------
1 | //! Common utilities.
2 | mod coord;
3 | mod error;
4 | mod size;
5 |
6 | pub use crate::common::coord::{Coord, CoordEnumerate, CoordEnumerateMut, CoordIter};
7 | pub use crate::common::error::{CommonError, Result};
8 | pub use crate::common::size::Size;
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ---
4 | ## [unreleased]
5 |
6 | ### Miscellaneous Chores
7 |
8 | - refactor ci config - ([891c54e](https://github.com/cocogitto/cocogitto/commit/891c54e76239912bd140872e4c3f1df7f169e2e2)) - Luiz F. A. de Prá
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [*.rs]
10 | max_line_length = 120
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 | indent_style = space
15 | indent_size = 2
16 |
17 | [*.{json,yml,yaml}]
18 | indent_style = space
19 | indent_size = 2
20 |
21 | [Makefile]
22 | indent_style = tab
23 |
--------------------------------------------------------------------------------
/tinlib/src/graphic/mod.rs:
--------------------------------------------------------------------------------
1 | //! Graphic utilities.
2 | mod color;
3 | mod font;
4 | mod glyph;
5 | mod palette;
6 |
7 | pub use crate::graphic::color::Color;
8 | pub use crate::graphic::font::{Font, FontGlyphIter, FontGlyphIterMut};
9 | pub use crate::graphic::glyph::{
10 | Glyph, GlyphPixel, GlyphPixelEnumerate, GlyphPixelEnumerateMut, GlyphPixelIter,
11 | GlyphPixelIterMut,
12 | };
13 | pub use crate::graphic::palette::{Palette, PaletteColorIter, PaletteColorIterMut};
14 |
--------------------------------------------------------------------------------
/devkit/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sn-50"
3 | version = "0.1.0"
4 | authors = ["Luiz F. A. de Prá "]
5 | description = "The SN-50 Fantasy Computer with DevKit tools."
6 | homepage = "https://github.com/TinTeam/SN-50/"
7 | repository = "https://github.com/TinTeam/SN-50/"
8 | documentation = "https://docs.rs/sn-50/"
9 | keywords = ["sn-50", "fantasy", "console", "computer"]
10 | categories = ["games", "game-development", "game-engines"]
11 | license = "MIT"
12 | readme = "README.md"
13 | edition = "2024"
14 | rust-version = "1.85.0"
15 |
16 | [dependencies]
17 | tinlib = { version = "0.1.0", path = "../tinlib" }
18 |
--------------------------------------------------------------------------------
/tinlib/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::result::Result as StdResult;
2 |
3 | pub mod cartridge;
4 | pub mod common;
5 | pub mod graphic;
6 | pub mod machine;
7 | pub mod map;
8 |
9 | use thiserror::Error;
10 |
11 | use crate::cartridge::CartridgeError;
12 | use crate::common::CommonError;
13 |
14 | /// Internal errors.
15 | #[derive(Error, Debug)]
16 | pub enum Error {
17 | /// Error to wrap internal Cartridge errors.
18 | #[error(transparent)]
19 | Cartridge(#[from] CartridgeError),
20 | /// Error to wrap internal Common errors.
21 | #[error(transparent)]
22 | Common(#[from] CommonError),
23 | }
24 |
25 | /// Internal result.
26 | pub type Result = StdResult;
27 |
--------------------------------------------------------------------------------
/tinlib/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tinlib"
3 | version = "0.1.0"
4 | authors = ["Luiz F. A. de Prá "]
5 | description = "Components for SN-50 Fantasy Computer implementations."
6 | homepage = "https://github.com/TinTeam/SN-50/"
7 | repository = "https://github.com/TinTeam/SN-50/"
8 | documentation = "https://docs.rs/tinlib/"
9 | keywords = ["sn-50", "fantasy", "console", "computer"]
10 | categories = ["games", "game-development", "game-engines"]
11 | license = "MIT"
12 | readme = "README.md"
13 | edition = "2024"
14 | rust-version = "1.85.0"
15 |
16 | [dependencies]
17 | byteorder = "^1.5"
18 | log = "^0.4"
19 | thiserror = "^2.0"
20 |
21 | [dev-dependencies]
22 | assert_matches = "^1.5"
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all install pre-commit check check-fmt check-clippy fix fix-fmt fix-clippy test test-cov build
2 |
3 | all: install fix test build
4 |
5 | install:
6 | cargo install git-cliff --locked
7 | cargo install cargo-llvm-cov --locked
8 | pre-commit install
9 |
10 | pre-commit:
11 | pre-commit run --all --verbose
12 |
13 | check: check-fmt check-clippy
14 |
15 | check-fmt:
16 | cargo fmt --all --check
17 |
18 | check-clippy:
19 | cargo clippy --all-targets --all-features --locked -- -D warnings
20 |
21 | fix: fix-fmt fix-clippy
22 |
23 | fix-fmt:
24 | cargo fmt --all
25 |
26 | fix-clippy:
27 | cargo clippy --all-targets --all-features --fix --locked
28 |
29 | test:
30 | cargo test --all-targets --locked
31 |
32 | test-cov:
33 | cargo llvm-cov --all --locked
34 |
35 | build:
36 | cargo build --all-targets --locked
37 |
--------------------------------------------------------------------------------
/player/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sn-50-player"
3 | version = "0.1.0"
4 | authors = ["Luiz F. A. de Prá "]
5 | description = "The SN-50 Fantasy Computer player."
6 | homepage = "https://github.com/TinTeam/SN-50/"
7 | repository = "https://github.com/TinTeam/SN-50/"
8 | documentation = "https://docs.rs/sn-50/"
9 | keywords = ["sn-50", "fantasy", "console", "computer"]
10 | categories = ["games", "game-development", "game-engines"]
11 | license = "MIT"
12 | readme = "README.md"
13 | edition = "2024"
14 | rust-version = "1.85.0"
15 |
16 | [dependencies]
17 | tinlib = { version = "0.1.0", path = "../tinlib" }
18 | winit = { version = "0.30", default-features = false, features = [
19 | "rwh_06",
20 | "x11",
21 | "wayland",
22 | "wayland-dlopen",
23 | "wayland-csd-adwaita",
24 | ] }
25 | env_logger = "0.11"
26 | error-iter = "0.4"
27 | anyhow = "1.0"
28 | log = "0.4"
29 | pixels = "0.15"
30 |
--------------------------------------------------------------------------------
/tinlib/src/machine/memory.rs:
--------------------------------------------------------------------------------
1 | //! Memory implementation and manipulation.
2 | use crate::machine::ram::RAM;
3 | use crate::machine::vram::VRAM;
4 |
5 | /// The machine Memory representation.
6 | pub struct Memory<'ram> {
7 | ram: RAM<'ram>,
8 | vram: VRAM,
9 | }
10 |
11 | impl<'ram> Memory<'ram> {
12 | /// Returns a ram reference.
13 | pub fn ram(&self) -> &RAM {
14 | &self.ram
15 | }
16 |
17 | /// Returns a mutable ram reference.
18 | pub fn ram_mut(&mut self) -> &mut RAM<'ram> {
19 | &mut self.ram
20 | }
21 |
22 | /// Returns a vram reference.
23 | pub fn vram(&self) -> &VRAM {
24 | &self.vram
25 | }
26 |
27 | /// Returns a mutable ram reference.
28 | pub fn vram_mut(&mut self) -> &mut VRAM {
29 | &mut self.vram
30 | }
31 | }
32 |
33 | impl Default for Memory<'_> {
34 | /// Creates a new Memory.
35 | fn default() -> Self {
36 | Self {
37 | ram: RAM::default(),
38 | vram: VRAM::default(),
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.5.0
4 | hooks:
5 | - id: check-executables-have-shebangs
6 | - id: check-json
7 | - id: check-shebang-scripts-are-executable
8 | - id: check-merge-conflict
9 | - id: check-symlinks
10 | - id: check-toml
11 | - id: check-yaml
12 | - id: detect-private-key
13 | - id: end-of-file-fixer
14 | - id: forbid-submodules
15 | - id: mixed-line-ending
16 | - id: mixed-line-ending
17 | - id: no-commit-to-branch
18 | - id: trailing-whitespace
19 |
20 | - repo: https://github.com/compilerla/conventional-pre-commit
21 | rev: v3.1.0
22 | hooks:
23 | - id: conventional-pre-commit
24 | stages: [commit-msg]
25 |
26 | - repo: https://github.com/doublify/pre-commit-rust
27 | rev: v1.0
28 | hooks:
29 | - id: fmt
30 | - id: cargo-check
31 | args: ['--all-targets', '--locked']
32 | - id: clippy
33 | args: ['--all-targets', '--all-features', '--locked', '--', '-D', 'warnings']
34 |
--------------------------------------------------------------------------------
/tinlib/src/machine/ram.rs:
--------------------------------------------------------------------------------
1 | //! RAM implementation and manipulation.
2 | use crate::machine::code::Code;
3 | use crate::machine::input::Input;
4 | use crate::map::Map;
5 |
6 | /// The machine RAM representation.
7 | #[derive(Default)]
8 | pub struct RAM<'map> {
9 | code: Code,
10 | map: Map<'map>,
11 | input: Input,
12 | }
13 |
14 | impl<'map> RAM<'map> {
15 | /// Returns a code reference.
16 | pub fn code(&self) -> &Code {
17 | &self.code
18 | }
19 |
20 | /// Returns a mutable code reference.
21 | pub fn code_mut(&mut self) -> &mut Code {
22 | &mut self.code
23 | }
24 |
25 | /// Returns a map reference.
26 | pub fn map(&self) -> &Map {
27 | &self.map
28 | }
29 |
30 | /// Returns a mutable map reference.
31 | pub fn map_mut(&mut self) -> &mut Map<'map> {
32 | &mut self.map
33 | }
34 |
35 | /// Returns an input reference.
36 | pub fn input(&self) -> &Input {
37 | &self.input
38 | }
39 |
40 | /// Returns a mutable input reference.
41 | pub fn input_mut(&mut self) -> &mut Input {
42 | &mut self.input
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Luiz F. A. de Prá
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 |
--------------------------------------------------------------------------------
/tinlib/src/machine/vram.rs:
--------------------------------------------------------------------------------
1 | //! VRAM implementation and manipulation.
2 | use crate::graphic::{Font, Palette};
3 | use crate::machine::screen::Screen;
4 |
5 | /// The machine VRAM representation.
6 | pub struct VRAM {
7 | screen: Screen,
8 | palette: Palette,
9 | font: Font,
10 | }
11 |
12 | impl VRAM {
13 | /// Returns a screen reference.
14 | pub fn screen(&self) -> &Screen {
15 | &self.screen
16 | }
17 |
18 | /// Returns a mutable screen reference.
19 | pub fn screen_mut(&mut self) -> &mut Screen {
20 | &mut self.screen
21 | }
22 |
23 | /// Returns a palette reference.
24 | pub fn palette(&self) -> &Palette {
25 | &self.palette
26 | }
27 |
28 | /// Returns a mutable palette reference.
29 | pub fn palette_mut(&mut self) -> &mut Palette {
30 | &mut self.palette
31 | }
32 |
33 | /// Returns a font reference.
34 | pub fn font(&self) -> &Font {
35 | &self.font
36 | }
37 |
38 | /// Returns a mutable font reference.
39 | pub fn font_mut(&mut self) -> &mut Font {
40 | &mut self.font
41 | }
42 | }
43 |
44 | impl Default for VRAM {
45 | /// Creates a new VRAM.
46 | fn default() -> Self {
47 | Self {
48 | screen: Screen::default(),
49 | palette: Palette::default(),
50 | font: Font::default(),
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tinlib/examples/cartridge.rs:
--------------------------------------------------------------------------------
1 | use std::io::Cursor;
2 |
3 | use tinlib::cartridge::Cartridge;
4 |
5 | fn main() {
6 | let cart = Cartridge {
7 | // An incomplete game cart with empty fonts, map and cover.
8 | version: 17,
9 | name: "Dungeons of the Dungeon".to_string(),
10 | desc: "A cool game about dungeons inside dungeons.".to_string(),
11 | author: "Luiz de Prá".to_string(),
12 | palette: vec![
13 | 0x2d, 0x1b, 0x000, // dark
14 | 0x1e, 0x60, 0x6e, // dark greenish
15 | 0x5a, 0xb9, 0xa8, // greenish
16 | 0xc4, 0xf0, 0xc2, // light greenish
17 | ],
18 | code: "def main:\n pass".to_string(),
19 | ..Default::default()
20 | };
21 |
22 | println!("Pre-save Cart: {:?}\n\n", &cart);
23 |
24 | // Saving the cart data into a cursor (file or anything that implements Write).
25 | let mut cursor = Cursor::new(vec![]);
26 | cart.save(&mut cursor).expect("failed to save cart");
27 |
28 | println!("File data: {:?}\n\n", &cursor);
29 |
30 | // Loading the cart data from a cursor (file, or anything that implements Read).
31 | cursor.set_position(0);
32 | let new_cart = Cartridge::from_reader(&mut cursor).expect("failed to load cart");
33 |
34 | println!("Post-load Cart: {:?}\n\n", &new_cart);
35 |
36 | println!("They has the same data? {}\n\n", cart == new_cart);
37 | }
38 |
--------------------------------------------------------------------------------
/tinlib/src/machine/mod.rs:
--------------------------------------------------------------------------------
1 | //! Machine utilities.
2 | mod code;
3 | mod input;
4 | mod memory;
5 | mod ram;
6 | mod screen;
7 | mod vram;
8 |
9 | pub use crate::machine::code::Code;
10 | pub use crate::machine::input::Input;
11 | pub use crate::machine::memory::Memory;
12 | pub use crate::machine::ram::RAM;
13 | pub use crate::machine::screen::{
14 | Screen, ScreenPixel, ScreenPixelEnumerate, ScreenPixelEnumerateMut, ScreenPixelIter,
15 | ScreenPixelIterMut,
16 | };
17 | pub use crate::machine::vram::VRAM;
18 |
19 | /// Machine states.
20 | #[derive(Debug, Clone, Copy, PartialEq)]
21 | pub enum MachineState {
22 | /// When the machine was just created and no cart was loaded yet.
23 | Created,
24 | /// When a cart was loaded but the machine is not running it yet.
25 | Loaded,
26 | /// When the machine is running the loaded cart.
27 | Started,
28 | /// When the cart execution is paused.
29 | Paused,
30 | }
31 |
32 | /// The machine representation.
33 | pub struct Machine<'mem> {
34 | state: MachineState,
35 | #[allow(dead_code)]
36 | memory: Memory<'mem>,
37 | }
38 |
39 | impl Machine<'_> {
40 | /// Returns the current state.
41 | pub fn state(&self) -> MachineState {
42 | self.state
43 | }
44 |
45 | pub fn load_cartridge(&mut self) {}
46 |
47 | pub fn start(&mut self) {}
48 |
49 | pub fn pause(&mut self) {}
50 |
51 | pub fn stop(&mut self) {}
52 | }
53 |
54 | impl Default for Machine<'_> {
55 | /// Creates a new Machine in the `Created` state.
56 | fn default() -> Self {
57 | Self {
58 | state: MachineState::Created,
59 | memory: Memory::default(),
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tinlib/src/common/size.rs:
--------------------------------------------------------------------------------
1 | //! Size implementation and manipulation.
2 |
3 | /// A Size implementation with `usize` dimensions.
4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
5 | pub struct Size {
6 | width: usize,
7 | height: usize,
8 | }
9 |
10 | impl Size {
11 | /// Creates a new Size.
12 | pub fn new(width: usize, height: usize) -> Self {
13 | Self { width, height }
14 | }
15 |
16 | /// Returns the width.
17 | pub fn width(&self) -> usize {
18 | self.width
19 | }
20 |
21 | /// Returns the height.
22 | pub fn height(&self) -> usize {
23 | self.height
24 | }
25 | }
26 |
27 | impl From<(usize, usize)> for Size {
28 | fn from((x, y): (usize, usize)) -> Self {
29 | Self::new(x, y)
30 | }
31 | }
32 |
33 | impl From<[usize; 2]> for Size {
34 | fn from(array: [usize; 2]) -> Self {
35 | Self::new(array[0], array[1])
36 | }
37 | }
38 |
39 | #[cfg(test)]
40 | mod test {
41 | use super::*;
42 |
43 | #[test]
44 | fn test_size_new() {
45 | let size = Size::new(80, 48);
46 |
47 | assert_eq!(size.width, 80);
48 | assert_eq!(size.height, 48);
49 | }
50 |
51 | #[test]
52 | fn test_size_width_and_height() {
53 | let size = Size::new(80, 48);
54 |
55 | assert_eq!(size.width(), 80);
56 | assert_eq!(size.height(), 48);
57 | }
58 |
59 | #[test]
60 | fn test_size_from_tuple() {
61 | let tuple = (80usize, 48usize);
62 | let size = Size::from(tuple);
63 |
64 | assert_eq!(size, Size::new(80, 48));
65 | }
66 |
67 | #[test]
68 | fn test_size_from_array() {
69 | let array = [80usize, 48usize];
70 | let size = Size::from(array);
71 |
72 | assert_eq!(size, Size::new(80, 48));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tinlib/src/common/error.rs:
--------------------------------------------------------------------------------
1 | //! CommonError implementation and manipulation.
2 | use std::result::Result as StdResult;
3 |
4 | use thiserror::Error;
5 |
6 | use crate::common::coord::Coord;
7 | use crate::common::size::Size;
8 |
9 | /// Common errors.
10 | #[derive(Error, Debug)]
11 | pub enum CommonError {
12 | /// Error to represent invalid coords.
13 | #[error("invalid coord ({coord:?}) for size ({size:?})")]
14 | InvalidCoord { coord: Coord, size: Size },
15 | /// Error to reprense invalid indexes.
16 | #[error("invalid index {index} for lenght {lenght}")]
17 | InvalidIndex { index: usize, lenght: usize },
18 | }
19 |
20 | impl CommonError {
21 | /// Creates a `InvalidCoord` error.
22 | pub fn new_invalid_coord(coord: Coord, size: Size) -> Self {
23 | Self::InvalidCoord { coord, size }
24 | }
25 |
26 | /// Creates a `InvalidIndex` error.
27 | pub fn new_invalid_index(index: usize, lenght: usize) -> Self {
28 | Self::InvalidIndex { index, lenght }
29 | }
30 | }
31 |
32 | pub type Result = StdResult;
33 |
34 | #[cfg(test)]
35 | mod test_super {
36 | use assert_matches::assert_matches;
37 |
38 | use super::*;
39 |
40 | #[test]
41 | fn test_commonerror_new_invalid_index() {
42 | let index = 2usize;
43 | let lenght = 1usize;
44 |
45 | let error = CommonError::new_invalid_index(index, lenght);
46 |
47 | assert_matches!(
48 | error,
49 | CommonError::InvalidIndex { index: i, lenght: l } if i == index && l == lenght
50 | );
51 | }
52 |
53 | #[test]
54 | fn test_commonerror_new_invalid_coord() {
55 | let coord = Coord::new(2, 2);
56 | let size: Size = Size::new(1, 1);
57 |
58 | let error = CommonError::new_invalid_coord(coord, size);
59 |
60 | assert_matches!(
61 | error,
62 | CommonError::InvalidCoord { coord: c, size: s } if c == coord && s == size
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tinlib/src/graphic/color.rs:
--------------------------------------------------------------------------------
1 | //! Color implementation and manipulation.
2 |
3 | /// A color representation with red, green and blue values.
4 | #[derive(Debug, Default, Clone, Copy, PartialEq)]
5 | pub struct Color {
6 | pub red: u8,
7 | pub green: u8,
8 | pub blue: u8,
9 | }
10 |
11 | impl Color {
12 | /// Creates a Color with red, green and blue values.
13 | pub fn new(red: u8, green: u8, blue: u8) -> Self {
14 | Self { red, green, blue }
15 | }
16 | }
17 |
18 | impl From for Color {
19 | fn from(value: u32) -> Self {
20 | Self {
21 | red: ((value & 0x00ff_0000) >> 16) as u8,
22 | green: ((value & 0x0000_ff00) >> 8) as u8,
23 | blue: (value & 0x0000_00ff) as u8,
24 | }
25 | }
26 | }
27 |
28 | impl From<(u8, u8, u8)> for Color {
29 | fn from(value: (u8, u8, u8)) -> Self {
30 | Self::new(value.0, value.1, value.2)
31 | }
32 | }
33 |
34 | impl From<[u8; 3]> for Color {
35 | fn from(array: [u8; 3]) -> Self {
36 | Self::new(array[0], array[1], array[2])
37 | }
38 | }
39 |
40 | #[cfg(test)]
41 | mod tests {
42 | use super::*;
43 |
44 | #[test]
45 | fn test_color_new() {
46 | let color = Color::new(1, 2, 3);
47 |
48 | assert_eq!(color.red, 1);
49 | assert_eq!(color.green, 2);
50 | assert_eq!(color.blue, 3);
51 | }
52 |
53 | #[test]
54 | fn test_color_new_from_hex() {
55 | let color = Color::from(0x7bc950);
56 |
57 | assert_eq!(color.red, 123);
58 | assert_eq!(color.green, 201);
59 | assert_eq!(color.blue, 80);
60 | }
61 |
62 | #[test]
63 | fn test_color_red_green_blue() {
64 | let color = Color::new(1, 2, 3);
65 |
66 | assert_eq!(color.red, 1);
67 | assert_eq!(color.green, 2);
68 | assert_eq!(color.blue, 3);
69 | }
70 |
71 | #[test]
72 | fn test_color_from_tuple() {
73 | let tuple = (1u8, 2u8, 3u8);
74 | let color = Color::from(tuple);
75 |
76 | assert_eq!(color.red, tuple.0);
77 | assert_eq!(color.green, tuple.1);
78 | assert_eq!(color.blue, tuple.2);
79 | }
80 |
81 | #[test]
82 | fn test_color_from_array() {
83 | let array = [1u8, 2u8, 3u8];
84 | let color = Color::from(array);
85 |
86 | assert_eq!(color.red, array[0]);
87 | assert_eq!(color.green, array[1]);
88 | assert_eq!(color.blue, array[2]);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog\n
4 | """
5 | body = """
6 | ---
7 | {% if version %}\
8 | {% if previous.version %}\
9 | ## [{{ version | trim_start_matches(pat="v") }}]($REPO/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
10 | {% else %}\
11 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
12 | {% endif %}\
13 | {% else %}\
14 | ## [unreleased]
15 | {% endif %}\
16 | {% for group, commits in commits | group_by(attribute="group") %}
17 | ### {{ group | striptags | trim | upper_first }}
18 | {% for commit in commits
19 | | filter(attribute="scope")
20 | | sort(attribute="scope") %}
21 | - **({{commit.scope}})**{% if commit.breaking %} [**breaking**]{% endif %} \
22 | {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
23 | {%- endfor -%}
24 | {% raw %}\n{% endraw %}\
25 | {%- for commit in commits %}
26 | {%- if commit.scope -%}
27 | {% else -%}
28 | - {% if commit.breaking %} [**breaking**]{% endif %}\
29 | {{ commit.message }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) - {{ commit.author.name }}
30 | {% endif -%}
31 | {% endfor -%}
32 | {% endfor %}\n
33 | """
34 | footer = """
35 |
36 | """
37 | trim = true
38 | postprocessors = [
39 | { pattern = '\$REPO', replace = "https://github.com/cocogitto/cocogitto" }, # replace repository URL
40 | ]
41 |
42 | [git]
43 | conventional_commits = true
44 | filter_unconventional = false
45 | split_commits = false
46 | commit_preprocessors = []
47 | commit_parsers = [
48 | { message = "^feat", group = "Features" },
49 | { message = "^fix", group = "Bug Fixes" },
50 | { message = "^doc", group = "Documentation" },
51 | { message = "^perf", group = "Performance" },
52 | { message = "^refactor", group = "Refactoring" },
53 | { message = "^style", group = "Style" },
54 | { message = "^revert", group = "Revert" },
55 | { message = "^test", group = "Tests" },
56 | { message = "^chore\\(version\\):", skip = true },
57 | { message = "^chore", group = "Miscellaneous Chores" },
58 | { body = ".*security", group = "Security" },
59 | ]
60 | protect_breaking_commits = false
61 | filter_commits = false
62 | tag_pattern = "v[0-9].*"
63 | skip_tags = "v0.1.0-beta.1"
64 | ignore_tags = ""
65 | topo_order = false
66 | sort_commits = "oldest"
67 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | fmt:
11 | name: Fmt
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout sources
16 | uses: actions/checkout@v4
17 |
18 | - name: Install toolchain
19 | uses: dtolnay/rust-toolchain@stable
20 | with:
21 | toolchain: stable
22 | components: rustfmt
23 |
24 | - name: Restore cache
25 | uses: Swatinem/rust-cache@v2
26 |
27 | - name: Run fmt
28 | run: cargo fmt --all --check
29 |
30 | clippy:
31 | name: Clippy
32 | runs-on: ubuntu-latest
33 |
34 | steps:
35 | - name: Checkout sources
36 | uses: actions/checkout@v4
37 |
38 | - name: Install toolchain
39 | uses: dtolnay/rust-toolchain@stable
40 | with:
41 | toolchain: stable
42 | components: clippy
43 |
44 | - name: Restore cache
45 | uses: Swatinem/rust-cache@v1
46 |
47 | - name: Run clippy
48 | run: cargo clippy --all-targets --all-features --locked -- -D warnings
49 |
50 | check:
51 | name: Check
52 |
53 | strategy:
54 | fail-fast: false
55 | matrix:
56 | rust:
57 | - stable
58 | - beta
59 | - nightly
60 | os:
61 | - ubuntu-latest
62 | # - macos-latest
63 | # - windows-latest
64 |
65 | runs-on: ${{ matrix.os }}
66 |
67 | steps:
68 | - name: Checkout sources
69 | uses: actions/checkout@v4
70 |
71 | - name: Install toolchain
72 | uses: dtolnay/rust-toolchain@stable
73 | with:
74 | toolchain: ${{ matrix.rust }}
75 |
76 | - name: Run check
77 | run: cargo check --all-targets --locked
78 |
79 | test:
80 | name: Test
81 |
82 | strategy:
83 | fail-fast: false
84 | matrix:
85 | rust:
86 | - stable
87 | - beta
88 | - nightly
89 | os:
90 | - ubuntu-latest
91 | # - macos-latest
92 | # - windows-latest
93 |
94 | runs-on: ${{ matrix.os }}
95 |
96 | steps:
97 | - name: Checkout sources
98 | uses: actions/checkout@v4
99 |
100 | - name: Install toolchain
101 | uses: dtolnay/rust-toolchain@stable
102 | with:
103 | toolchain: ${{ matrix.rust }}
104 |
105 | - name: Run test
106 | run: cargo test --all-targets --locked
107 |
108 | coverage:
109 | name: Coverage
110 |
111 | runs-on: ubuntu-latest
112 |
113 | steps:
114 | - name: Checkout sources
115 | uses: actions/checkout@v4
116 |
117 | - name: Install toolchain
118 | uses: dtolnay/rust-toolchain@nightly
119 | with:
120 | components: llvm-tools
121 |
122 | - name: Install llvm-cov
123 | uses: taiki-e/install-action@v2
124 | with:
125 | tool: cargo-llvm-cov
126 | - run: cargo llvm-cov --all-features --lcov --output-path lcov.info
127 |
128 | - name: Upload coverage to coveralls
129 | uses: coverallsapp/github-action@v2
130 | with:
131 | github-token: ${{ secrets.GITHUB_TOKEN }}
132 | path-to-lcov: lcov.info
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | SN-50
27 |
28 |
29 | A fun Fantasy Computer
30 |
31 |
32 |
33 | ## Table of Contents
34 |
35 | - [About SN-50](#about-sn-50)
36 | - [Tailored Limitations](#tailored-limitations)
37 | - [Features](#features)
38 | - [Getting Started](#getting-started)
39 | - [Releases](#releases)
40 | - [Building](#building)
41 | - [Usage](#usage)
42 | - [Contributing](#contributing)
43 | - [License](#license)
44 | - [Acknowledgements](#acknowledgements)
45 |
46 | ## About SN-50
47 |
48 | SN-50 is a free and open source fantasy computer for building, playing and sharing resources-limited games. The game limitations were inspired in old computers and their text-based games.
49 |
50 | The project is basically a simple console and several tools for building games, as such: code, glyph, sound and music editors. Games are saved, packed and distributed in cartridge files. These files can be executed by the SN-50 or any player that implements the console specifications.
51 |
52 | ### Tailored Limitations
53 |
54 | TBD
55 |
56 | ### Features
57 |
58 | TBD
59 |
60 | ## Getting Started
61 |
62 | To start using SN-50 you can download a released binary or build it yourself.
63 |
64 | ### Releases
65 |
66 | You can download all the compiled version os SN-50 in the [release page][releases].
67 |
68 | ### Building
69 |
70 | Follow these steps to build the project:
71 |
72 | **Important:** You must have the latest Rust version installed to build this project.
73 |
74 | 1. Clone the repo and move to the project's folder
75 | ```sh
76 | git clone https://github.com/TinTeam/SN-50.git && cd SN-50
77 | ```
78 | 2. Build the project with `cargo`
79 | ```sh
80 | cargo build --release
81 | ```
82 |
83 | ## Usage
84 |
85 | TBD
86 |
87 | ## Contributing
88 |
89 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
90 |
91 | 1. Fork the Project
92 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
93 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
94 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
95 | 5. Open a Pull Request
96 |
97 | ## License
98 |
99 | Distributed under the MIT License. See `LICENSE` for more information.
100 |
101 | ## Acknowledgements
102 |
103 | * [Elias "dlight/amiguxo"](https://github.com/dlight/), thank you for helping me solve tricky problems with lifetimes and giving advices.
104 |
105 | [releases]: https://github.com/TinTeam/SN-50/releases
106 |
--------------------------------------------------------------------------------
/tinlib/src/cartridge/error.rs:
--------------------------------------------------------------------------------
1 | //! CartridgeError implementation and manipulation.
2 | use std::io;
3 | use std::result::Result as StdResult;
4 | use std::string::FromUtf8Error;
5 |
6 | use thiserror::Error;
7 |
8 | use crate::cartridge::chunk::ChunkType;
9 |
10 | /// Cartridge errors.
11 | #[derive(Error, Debug)]
12 | pub enum CartridgeError {
13 | /// Error to represent invalid chunk types.
14 | #[error("invalid chunk type {0}")]
15 | InvalidChunkType(u8),
16 | /// Error to represent invalid chunk sizes.
17 | #[error("invalid chunk size {1} for type {0:?}, expected: {2:?}")]
18 | InvalidChunkSize(ChunkType, usize, Vec),
19 | /// Error to represent invalid chunk max sizes.
20 | #[error("invalid chunk size {1} for type {0:?}, max expected: {2}")]
21 | InvalidChunkMaxSize(ChunkType, usize, usize),
22 | /// Error to represent mismatched chunk sizes.
23 | #[error("mismatched chunk header size {1} and data sizes {2} for type {0:?}")]
24 | MismatchedChunkSizes(ChunkType, usize, usize),
25 | /// Error to wrap an invalid conversion to UTF8.
26 | #[error("UFT8 conversion error")]
27 | FromUtf8(#[from] FromUtf8Error),
28 | /// Error to wrap `io::Error`s from loading process.
29 | #[error("IO operation error")]
30 | Io(#[from] io::Error),
31 | }
32 |
33 | impl CartridgeError {
34 | /// Creates a `InvalidChunkType` error.
35 | pub fn new_invalid_chunk_type(chunk_type: u8) -> Self {
36 | Self::InvalidChunkType(chunk_type)
37 | }
38 |
39 | /// Creates a `InvalidChunkSize` error.
40 | pub fn new_invalid_chunk_size(
41 | chunk_type: ChunkType,
42 | value: usize,
43 | expected: Vec,
44 | ) -> Self {
45 | Self::InvalidChunkSize(chunk_type, value, expected)
46 | }
47 |
48 | /// Creates a `InvalidChunkMaxSize` error.
49 | pub fn new_invalid_chunk_max_size(
50 | chunk_type: ChunkType,
51 | value: usize,
52 | expected: usize,
53 | ) -> Self {
54 | Self::InvalidChunkMaxSize(chunk_type, value, expected)
55 | }
56 |
57 | /// Creates a `MismatchedChunkSizes` error.
58 | pub fn new_mismatched_chunk_sizes(
59 | chunk_type: ChunkType,
60 | header_size: usize,
61 | data_size: usize,
62 | ) -> Self {
63 | Self::MismatchedChunkSizes(chunk_type, header_size, data_size)
64 | }
65 | }
66 |
67 | pub type Result = StdResult;
68 |
69 | #[cfg(test)]
70 | mod test_super {
71 | use assert_matches::assert_matches;
72 |
73 | use super::*;
74 |
75 | #[test]
76 | fn test_cartridgeerror_new_invalid_chunk_type() {
77 | let chunk_type = 99u8;
78 |
79 | let error = CartridgeError::new_invalid_chunk_type(chunk_type);
80 |
81 | assert_matches!(
82 | error,
83 | CartridgeError::InvalidChunkType(ct) if ct == chunk_type
84 | );
85 | }
86 |
87 | #[test]
88 | fn test_cartridgeerror_new_invalid_chunk_size() {
89 | let chunk_type = ChunkType::End;
90 | let value = 1usize;
91 | let expected = vec![0usize];
92 |
93 | let error = CartridgeError::new_invalid_chunk_size(chunk_type, value, expected.clone());
94 |
95 | assert_matches!(
96 | error,
97 | CartridgeError::InvalidChunkSize(ct, v, e) if ct == chunk_type && v == value && e == expected
98 | );
99 | }
100 |
101 | #[test]
102 | fn test_cartridgeerror_new_invalid_chunk_max_size() {
103 | let chunk_type = ChunkType::Code;
104 | let value = 140000usize;
105 | let expected = 131072usize;
106 |
107 | let error = CartridgeError::new_invalid_chunk_max_size(chunk_type, value, expected);
108 |
109 | assert_matches!(
110 | error,
111 | CartridgeError::InvalidChunkMaxSize(ct, v, e) if ct == chunk_type && v == value && e == expected
112 | );
113 | }
114 |
115 | #[test]
116 | fn test_cartridgeerror_new_mismatched_chunk_sizes() {
117 | let chunk_type = ChunkType::Code;
118 | let header_size = 10usize;
119 | let data_size = 15usize;
120 |
121 | let error = CartridgeError::new_mismatched_chunk_sizes(chunk_type, header_size, data_size);
122 |
123 | assert_matches!(
124 | error,
125 | CartridgeError::MismatchedChunkSizes(ct, h, d) if ct == chunk_type && h == header_size && d == data_size
126 | );
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/tinlib/src/graphic/palette.rs:
--------------------------------------------------------------------------------
1 | //! Palette implementation and manipulation.
2 | use std::fmt;
3 | use std::slice;
4 |
5 | use crate::common::{CommonError, Result};
6 | use crate::graphic::color::Color;
7 |
8 | /// Default number of colors in a Palette.
9 | const NUM_COLORS_IN_PALETTE: usize = 16;
10 |
11 | /// A iterator over all palette colors.
12 | pub type PaletteColorIter<'iter> = slice::Iter<'iter, Color>;
13 | /// A mutable iterator over all palette colors.
14 | pub type PaletteColorIterMut<'iter> = slice::IterMut<'iter, Color>;
15 |
16 | /// A Palette representation with N colors.
17 | #[derive(Clone)]
18 | pub struct Palette {
19 | /// Palette's colors.
20 | colors: Vec,
21 | }
22 |
23 | impl Palette {
24 | /// Creates a new Palette.
25 | pub fn new(num_colors: usize) -> Self {
26 | Self {
27 | colors: vec![Color::default(); num_colors],
28 | }
29 | }
30 |
31 | /// Returns the lenght.
32 | pub fn lenght(&self) -> usize {
33 | self.colors.len()
34 | }
35 |
36 | /// Returns a color.
37 | pub fn get_color(&self, index: usize) -> Result {
38 | if !self.is_index_valid(index) {
39 | return Err(CommonError::new_invalid_index(index, self.lenght()));
40 | }
41 |
42 | Ok(self.colors[index])
43 | }
44 |
45 | /// Sets a color.
46 | pub fn set_color(&mut self, index: usize, color: Color) -> Result<()> {
47 | if !self.is_index_valid(index) {
48 | return Err(CommonError::new_invalid_index(index, self.lenght()));
49 | }
50 |
51 | self.colors[index] = color;
52 |
53 | Ok(())
54 | }
55 |
56 | /// Returns an iterator over all palette pixels.
57 | pub fn iter(&self) -> PaletteColorIter {
58 | self.colors.iter()
59 | }
60 |
61 | /// Returns a mutable iterator over all palette pixels.
62 | pub fn iter_mut(&mut self) -> PaletteColorIterMut {
63 | self.colors.iter_mut()
64 | }
65 |
66 | fn is_index_valid(&self, index: usize) -> bool {
67 | index < self.lenght()
68 | }
69 | }
70 |
71 | impl Default for Palette {
72 | /// Creates a Palette with all colors set to black.
73 | fn default() -> Self {
74 | Self {
75 | colors: vec![Color::default(); NUM_COLORS_IN_PALETTE],
76 | }
77 | }
78 | }
79 |
80 | impl fmt::Debug for Palette {
81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82 | let data: Vec<&Color> = self.colors.iter().collect();
83 |
84 | f.debug_struct("Palette").field("colors", &data).finish()
85 | }
86 | }
87 |
88 | impl From<&[Color]> for Palette {
89 | fn from(colors: &[Color]) -> Self {
90 | Self {
91 | colors: colors.to_vec(),
92 | }
93 | }
94 | }
95 |
96 | #[cfg(test)]
97 | mod tests {
98 | use assert_matches::assert_matches;
99 |
100 | use super::*;
101 |
102 | #[test]
103 | fn test_palette_default() {
104 | let palette = Palette::default();
105 | assert_eq!(palette.colors.len(), NUM_COLORS_IN_PALETTE);
106 | }
107 |
108 | #[test]
109 | fn test_palette_len() {
110 | let palette = Palette::default();
111 | assert_eq!(palette.lenght(), NUM_COLORS_IN_PALETTE);
112 | }
113 |
114 | #[test]
115 | fn test_palette_get_color() {
116 | let palette = Palette::default();
117 | let color = Color::default();
118 |
119 | let result = palette.get_color(0);
120 | assert!(result.is_ok());
121 | assert_eq!(result.unwrap(), color);
122 | }
123 |
124 | #[test]
125 | fn test_palette_get_color_invalid_index() {
126 | let palette = Palette::default();
127 | let index = 16usize;
128 |
129 | let result = palette.get_color(index);
130 | assert!(result.is_err());
131 | assert_matches!(
132 | result.unwrap_err(),
133 | CommonError::InvalidIndex { index: i, lenght: l } if i == index && l == palette.lenght()
134 | );
135 | }
136 |
137 | #[test]
138 | fn test_palette_set_color() {
139 | let mut palette = Palette::default();
140 | let color = Color::new(255, 255, 255);
141 |
142 | let result = palette.set_color(0, color);
143 | assert!(result.is_ok());
144 |
145 | let result = palette.get_color(0);
146 | assert_eq!(result.unwrap(), color);
147 | }
148 |
149 | #[test]
150 | fn test_palette_set_color_invalid_index() {
151 | let mut palette = Palette::default();
152 | let color = Color::new(255, 255, 255);
153 | let index = 16usize;
154 |
155 | let result = palette.set_color(16, color);
156 | assert!(result.is_err());
157 | assert_matches!(
158 | result.unwrap_err(),
159 | CommonError::InvalidIndex { index: i, lenght: l } if i == index && l == palette.lenght()
160 | );
161 | }
162 |
163 | #[test]
164 | fn test_palette_iter() {
165 | let palette = Palette::default();
166 | let default_color = Color::default();
167 |
168 | for color in palette.iter() {
169 | assert_eq!(color, &default_color);
170 | }
171 | }
172 |
173 | #[test]
174 | fn test_palette_iter_mut() {
175 | let mut palette = Palette::default();
176 | let new_color = Color::new(255, 255, 255);
177 |
178 | for color in palette.iter_mut() {
179 | *color = new_color;
180 | }
181 |
182 | for color in palette.iter() {
183 | assert_eq!(color, &new_color);
184 | }
185 | }
186 |
187 | #[test]
188 | fn test_palette_debug() {
189 | let palette = Palette::default();
190 | let data: Vec<&Color> = palette.colors.iter().collect();
191 |
192 | let expected = format!("Palette {{ colors: {:?} }}", data);
193 | let result = format!("{:?}", palette);
194 |
195 | assert_eq!(result, expected);
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/player/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | array,
3 | sync::Arc,
4 | time::{Duration, Instant},
5 | };
6 |
7 | use anyhow::Result;
8 | use log::{error, info};
9 | use pixels::{Pixels, SurfaceTexture};
10 | use winit::{
11 | application::ApplicationHandler,
12 | dpi::LogicalSize,
13 | event::{ElementState, KeyEvent, WindowEvent},
14 | event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
15 | keyboard::{Key, NamedKey},
16 | window::{Window, WindowId},
17 | };
18 |
19 | const WINDOW_WIDTH: u32 = 640;
20 | const WINDOW_HEIGHT: u32 = 360;
21 |
22 | const BUFFER_SIZE: usize = (WINDOW_WIDTH * WINDOW_HEIGHT * 4) as usize;
23 | const BLACK_COLOR: [u8; 4] = [0x00, 0x00, 0x00, 0xFF];
24 | const COLOR_SIZE: usize = 4;
25 |
26 | const TARGET_FPS: f64 = 60.0;
27 | const TARGET_FRAME_TIME: f64 = 1.0 / TARGET_FPS;
28 |
29 | struct GamePlayer<'win> {
30 | pixels: Option>,
31 | window: Option>,
32 | buffer: [u8; BUFFER_SIZE],
33 | last_color: [u8; 4],
34 | should_exit: bool,
35 | is_paused: bool,
36 | previous_instant: Instant,
37 | current_instant: Instant,
38 | }
39 |
40 | impl GamePlayer<'_> {
41 | fn new() -> Self {
42 | let color = [0x00, 0x00, 0x00, 0xFF];
43 | Self {
44 | pixels: None,
45 | window: None,
46 | buffer: array::from_fn(|i| color[i % color.len()]),
47 | last_color: color,
48 | should_exit: false,
49 | is_paused: false,
50 | previous_instant: Instant::now(),
51 | current_instant: Instant::now(),
52 | }
53 | }
54 |
55 | fn update(&mut self) {
56 | let last_value = self.last_color.len() - 1;
57 | for (i, v) in self.last_color.iter_mut().enumerate() {
58 | if i != last_value {
59 | *v = if *v == 0xFF { 0x00 } else { *v + 1 }
60 | }
61 | }
62 |
63 | for (i, pixel) in self.buffer.chunks_exact_mut(COLOR_SIZE).enumerate() {
64 | let x = i % WINDOW_WIDTH as usize;
65 | let y = i / WINDOW_WIDTH as usize;
66 |
67 | pixel.copy_from_slice(if (x / 16) % 2 == (y / 16) % 2 {
68 | &self.last_color
69 | } else {
70 | &BLACK_COLOR
71 | });
72 | }
73 | }
74 |
75 | fn draw(&mut self) {
76 | let frame = self.pixels.as_mut().unwrap().frame_mut();
77 | for (i, pixel) in frame.chunks_exact_mut(COLOR_SIZE).enumerate() {
78 | let color = &self.buffer[i * COLOR_SIZE..(i + 1) * COLOR_SIZE];
79 | pixel.copy_from_slice(color);
80 | }
81 | }
82 |
83 | fn handle_input(&mut self, event: &KeyEvent) {
84 | if event.state == ElementState::Pressed && !event.repeat {
85 | match event.logical_key {
86 | Key::Named(NamedKey::Escape) => self.should_exit = true,
87 | Key::Named(NamedKey::Space) => self.is_paused = !self.is_paused,
88 | _ => {}
89 | }
90 | }
91 | }
92 |
93 | fn handle_update(&mut self) {
94 | if !self.is_paused {
95 | self.update();
96 | }
97 | }
98 |
99 | fn handle_drawing(&mut self) {
100 | self.draw();
101 | if let Err(err) = self.pixels.as_ref().unwrap().render() {
102 | error!("{err:?}");
103 | self.should_exit = true;
104 | }
105 | }
106 | }
107 |
108 | impl ApplicationHandler for GamePlayer<'_> {
109 | fn resumed(&mut self, event_loop: &ActiveEventLoop) {
110 | let size = LogicalSize::new(WINDOW_WIDTH as f64, WINDOW_HEIGHT as f64);
111 | let window_attributes = Window::default_attributes()
112 | .with_title("SN-50 Player")
113 | .with_inner_size(size)
114 | .with_resizable(false);
115 | let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
116 |
117 | let window_size = window.inner_size();
118 | let surface_texture =
119 | SurfaceTexture::new(window_size.width, window_size.height, window.clone());
120 | let pixels = Pixels::new(WINDOW_WIDTH, WINDOW_HEIGHT, surface_texture).unwrap();
121 |
122 | self.pixels = Some(pixels);
123 | self.window = Some(window);
124 | }
125 |
126 | fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
127 | if self.should_exit {
128 | event_loop.exit();
129 | return;
130 | }
131 |
132 | match event {
133 | WindowEvent::CloseRequested => {
134 | self.should_exit = true;
135 | }
136 | WindowEvent::KeyboardInput {
137 | event: key_event, ..
138 | } => {
139 | self.handle_input(&key_event);
140 | }
141 | WindowEvent::RedrawRequested => {
142 | self.current_instant = Instant::now();
143 | let elapsed = self
144 | .current_instant
145 | .duration_since(self.previous_instant)
146 | .as_secs_f64();
147 | self.previous_instant = self.current_instant;
148 | info!("Elapsed: {}", elapsed);
149 |
150 | self.handle_update();
151 | self.handle_drawing();
152 |
153 | let delay = TARGET_FRAME_TIME - elapsed;
154 | info!("Delay: {}", delay);
155 | if delay > 0.0 {
156 | std::thread::sleep(Duration::from_secs_f64(delay));
157 | }
158 |
159 | info!("FPS: {}", 1.0 / (elapsed + delay));
160 |
161 | self.window.as_ref().unwrap().request_redraw();
162 | }
163 | _ => (),
164 | }
165 | }
166 | }
167 |
168 | fn main() -> Result<()> {
169 | env_logger::init();
170 |
171 | let event_loop = EventLoop::new().unwrap();
172 | event_loop.set_control_flow(ControlFlow::Poll);
173 |
174 | let mut player = GamePlayer::new();
175 | event_loop.run_app(&mut player)?;
176 |
177 | Ok(())
178 | }
179 |
--------------------------------------------------------------------------------
/tinlib/src/common/coord.rs:
--------------------------------------------------------------------------------
1 | //! Coord implementation and manipulation.
2 | use std::slice;
3 |
4 | use crate::common::size::Size;
5 |
6 | /// A Coord representation.
7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
8 | pub struct Coord {
9 | pub x: usize,
10 | pub y: usize,
11 | }
12 |
13 | impl Coord {
14 | /// Creates a new Coord.
15 | pub fn new(x: usize, y: usize) -> Self {
16 | Self { x, y }
17 | }
18 | }
19 |
20 | impl From<(usize, usize)> for Coord {
21 | fn from((x, y): (usize, usize)) -> Self {
22 | Self::new(x, y)
23 | }
24 | }
25 |
26 | impl From<[usize; 2]> for Coord {
27 | fn from(array: [usize; 2]) -> Self {
28 | Self::new(array[0], array[1])
29 | }
30 | }
31 |
32 | /// A iterator over all Coord limited by Size.
33 | pub struct CoordIter {
34 | size: Size,
35 | coord: Coord,
36 | }
37 |
38 | impl CoordIter {
39 | // Creates a new CoordIter from a Size.
40 | pub fn new(size: Size) -> Self {
41 | Self {
42 | size,
43 | coord: Coord::new(0, 0),
44 | }
45 | }
46 | }
47 |
48 | impl Iterator for CoordIter {
49 | type Item = Coord;
50 |
51 | fn next(&mut self) -> Option {
52 | if self.coord.x == self.size.height() {
53 | return None;
54 | }
55 |
56 | let result = self.coord;
57 |
58 | self.coord.y += 1;
59 | if self.coord.y == self.size.width() {
60 | self.coord.y = 0;
61 | self.coord.x += 1;
62 | }
63 |
64 | Some(result)
65 | }
66 | }
67 |
68 | /// A iterator over all Coord and their related itens, limited by Size.
69 | pub struct CoordEnumerate<'iter, T: 'iter> {
70 | coords: CoordIter,
71 | iter: slice::Iter<'iter, T>,
72 | }
73 |
74 | impl<'iter, T> CoordEnumerate<'iter, T> {
75 | /// Creates a CoordEnumerate from a CoordIter and item Iter.
76 | pub fn new(coords: CoordIter, iter: slice::Iter<'iter, T>) -> Self {
77 | Self { coords, iter }
78 | }
79 | }
80 |
81 | impl<'iter, T> Iterator for CoordEnumerate<'iter, T> {
82 | type Item = (Coord, &'iter T);
83 |
84 | fn next(&mut self) -> Option {
85 | self.coords
86 | .next()
87 | .and_then(|c| self.iter.next().map(|t| (c, t)))
88 | }
89 | }
90 |
91 | /// A mutable iterator over all Coord and their related itens, limited by Size.
92 | pub struct CoordEnumerateMut<'iter, T: 'iter> {
93 | coords: CoordIter,
94 | iter: slice::IterMut<'iter, T>,
95 | }
96 |
97 | impl<'iter, T> CoordEnumerateMut<'iter, T> {
98 | /// Creates a CoordEnumerateMut from a CoordIter and item Iter.
99 | pub fn new(coords: CoordIter, iter: slice::IterMut<'iter, T>) -> Self {
100 | Self { coords, iter }
101 | }
102 | }
103 |
104 | impl<'iter, T> Iterator for CoordEnumerateMut<'iter, T> {
105 | type Item = (Coord, &'iter mut T);
106 | fn next(&mut self) -> Option {
107 | self.coords
108 | .next()
109 | .and_then(|c| self.iter.next().map(|t| (c, t)))
110 | }
111 | }
112 |
113 | #[cfg(test)]
114 | mod test {
115 | use super::*;
116 |
117 | #[test]
118 | fn test_coord_new() {
119 | let coord = Coord::new(11, 27);
120 |
121 | assert_eq!(coord.x, 11);
122 | assert_eq!(coord.y, 27);
123 | }
124 |
125 | #[test]
126 | fn test_coord_from_tuple() {
127 | let tuple = (11usize, 27usize);
128 | let coord = Coord::from(tuple);
129 |
130 | assert_eq!(coord, Coord::new(11, 27));
131 | }
132 |
133 | #[test]
134 | fn test_coord_from_array() {
135 | let array = [11usize, 27usize];
136 | let coord = Coord::from(array);
137 |
138 | assert_eq!(coord, Coord::new(11, 27));
139 | }
140 |
141 | #[test]
142 | fn test_coorditer_new() {
143 | let size = Size::new(3, 2);
144 | let coord = Coord::new(0, 0);
145 | let iter = CoordIter::new(size);
146 |
147 | assert_eq!(iter.size, size);
148 | assert_eq!(iter.coord, coord);
149 | }
150 |
151 | #[test]
152 | fn test_coorditer_next() {
153 | let size = Size::new(3, 2);
154 | let mut iter = CoordIter::new(size);
155 |
156 | assert_eq!(iter.next(), Some(Coord::new(0, 0)));
157 | assert_eq!(iter.next(), Some(Coord::new(0, 1)));
158 | assert_eq!(iter.next(), Some(Coord::new(0, 2)));
159 | assert_eq!(iter.next(), Some(Coord::new(1, 0)));
160 | assert_eq!(iter.next(), Some(Coord::new(1, 1)));
161 | assert_eq!(iter.next(), Some(Coord::new(1, 2)));
162 | assert_eq!(iter.next(), None);
163 | }
164 |
165 | #[test]
166 | fn test_coordenumerate_new_and_next() {
167 | let items = [1, 2, 3, 4, 5, 6];
168 | let size = Size::new(3, 2);
169 | let coorditer = CoordIter::new(size);
170 | let itemiter = items.iter();
171 | let mut enumerate = CoordEnumerate::new(coorditer, itemiter);
172 |
173 | assert_eq!(enumerate.next(), Some((Coord::new(0, 0), &1)));
174 | assert_eq!(enumerate.next(), Some((Coord::new(0, 1), &2)));
175 | assert_eq!(enumerate.next(), Some((Coord::new(0, 2), &3)));
176 | assert_eq!(enumerate.next(), Some((Coord::new(1, 0), &4)));
177 | assert_eq!(enumerate.next(), Some((Coord::new(1, 1), &5)));
178 | assert_eq!(enumerate.next(), Some((Coord::new(1, 2), &6)));
179 | assert_eq!(enumerate.next(), None);
180 | }
181 |
182 | #[test]
183 | fn test_coordenumeratemut_new_and_next() {
184 | let mut items = [1, 2, 3, 4, 5, 6];
185 | let size = Size::new(3, 2);
186 | let coorditer = CoordIter::new(size);
187 | let itemiter = items.iter_mut();
188 | let mut enumerate = CoordEnumerateMut::new(coorditer, itemiter);
189 |
190 | assert_eq!(enumerate.next(), Some((Coord::new(0, 0), &mut 1)));
191 | assert_eq!(enumerate.next(), Some((Coord::new(0, 1), &mut 2)));
192 | assert_eq!(enumerate.next(), Some((Coord::new(0, 2), &mut 3)));
193 | assert_eq!(enumerate.next(), Some((Coord::new(1, 0), &mut 4)));
194 | assert_eq!(enumerate.next(), Some((Coord::new(1, 1), &mut 5)));
195 | assert_eq!(enumerate.next(), Some((Coord::new(1, 2), &mut 6)));
196 | assert_eq!(enumerate.next(), None);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/tinlib/src/graphic/font.rs:
--------------------------------------------------------------------------------
1 | //! Font implementation and manipulation.
2 | use std::fmt;
3 | use std::slice;
4 |
5 | use crate::common::Size;
6 | use crate::common::{CommonError, Result};
7 | use crate::graphic::glyph::Glyph;
8 |
9 | /// Default number of Glyphs in a Font.
10 | const NUM_GLYPHS_IN_FONT: usize = 256;
11 | /// Default glyph width.
12 | const GLYPH_WIDTH: usize = 16;
13 | /// Default glyph height.
14 | const GLYPH_HEIGHT: usize = 16;
15 |
16 | /// A iterator over all font glyphs.
17 | pub type FontGlyphIter<'iter> = slice::Iter<'iter, Glyph>;
18 | /// A mutable iterator over all font glyphs.
19 | pub type FontGlyphIterMut<'iter> = slice::IterMut<'iter, Glyph>;
20 |
21 | /// A Font representation with N Glyphs.
22 | #[derive(Clone)]
23 | pub struct Font {
24 | /// Font's glyph size.
25 | glyph_size: Size,
26 | /// Font's glyphs.
27 | glyphs: Vec,
28 | }
29 |
30 | impl Font {
31 | /// Creates a new Font.
32 | pub fn new(glyph_size: Size, num_glyphs: usize) -> Self {
33 | Self {
34 | glyph_size,
35 | glyphs: vec![Glyph::default(); num_glyphs],
36 | }
37 | }
38 |
39 | /// Returns glyph's size.
40 | pub fn glyph_size(&self) -> Size {
41 | self.glyph_size
42 | }
43 |
44 | /// Returns the lenght.
45 | pub fn lenght(&self) -> usize {
46 | self.glyphs.len()
47 | }
48 |
49 | /// Returns a glyph.
50 | pub fn get_glyph(&self, index: usize) -> Result<&Glyph> {
51 | if !self.is_index_valid(index) {
52 | return Err(CommonError::new_invalid_index(index, self.lenght()));
53 | }
54 |
55 | Ok(&self.glyphs[index])
56 | }
57 |
58 | /// Returns a mutable glyph.
59 | pub fn get_glyph_mut(&mut self, index: usize) -> Result<&mut Glyph> {
60 | if !self.is_index_valid(index) {
61 | return Err(CommonError::new_invalid_index(index, self.lenght()));
62 | }
63 |
64 | Ok(&mut self.glyphs[index])
65 | }
66 |
67 | /// Sets a glyph.
68 | pub fn set_glyph(&mut self, index: usize, glyph: Glyph) -> Result<()> {
69 | if !self.is_index_valid(index) {
70 | return Err(CommonError::new_invalid_index(index, self.lenght()));
71 | }
72 |
73 | self.glyphs[index] = glyph;
74 |
75 | Ok(())
76 | }
77 |
78 | /// Returns an iterator over all font glyphs.
79 | pub fn iter(&self) -> FontGlyphIter {
80 | self.glyphs.iter()
81 | }
82 |
83 | /// Returns a mutable iterator over all font glyphs.
84 | pub fn iter_mut(&mut self) -> FontGlyphIterMut {
85 | self.glyphs.iter_mut()
86 | }
87 |
88 | fn is_index_valid(&self, index: usize) -> bool {
89 | index < self.lenght()
90 | }
91 | }
92 |
93 | impl Default for Font {
94 | /// Creates a Font with default empty glyphs.
95 | fn default() -> Self {
96 | Self {
97 | glyph_size: Size::new(GLYPH_WIDTH, GLYPH_HEIGHT),
98 | glyphs: vec![Glyph::default(); NUM_GLYPHS_IN_FONT],
99 | }
100 | }
101 | }
102 |
103 | impl fmt::Debug for Font {
104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105 | let data: Vec<&Glyph> = self.glyphs.iter().collect();
106 |
107 | f.debug_struct("Font").field("data", &data).finish()
108 | }
109 | }
110 |
111 | #[cfg(test)]
112 | mod tests {
113 | use assert_matches::assert_matches;
114 |
115 | use crate::common::Coord;
116 | use crate::graphic::glyph::GlyphPixel;
117 |
118 | use super::*;
119 |
120 | #[test]
121 | fn test_font_default() {
122 | let font = Font::default();
123 | assert_eq!(font.glyphs.len(), NUM_GLYPHS_IN_FONT);
124 | }
125 |
126 | #[test]
127 | fn test_font_len() {
128 | let font = Font::default();
129 | assert_eq!(font.lenght(), NUM_GLYPHS_IN_FONT);
130 | }
131 |
132 | #[test]
133 | fn test_font_get_glyph() {
134 | let font = Font::default();
135 | let glyph = Glyph::default();
136 |
137 | let result = font.get_glyph(0);
138 | assert!(result.is_ok());
139 | assert_eq!(result.unwrap(), &glyph);
140 | }
141 |
142 | #[test]
143 | fn test_font_get_glyph_invalid_index() {
144 | let font = Font::default();
145 | let index = 256usize;
146 |
147 | let result = font.get_glyph(index);
148 | assert!(result.is_err());
149 | assert_matches!(
150 | result.unwrap_err(),
151 | CommonError::InvalidIndex { index: i, lenght: l } if i == index && l == font.lenght()
152 | );
153 | }
154 |
155 | #[test]
156 | fn test_font_set_glyph() {
157 | let mut font = Font::default();
158 |
159 | let coord = Coord::new(0, 0);
160 | let mut new_glyph = Glyph::default();
161 | new_glyph.set_pixel(coord, GlyphPixel::Solid).unwrap();
162 |
163 | let result = font.set_glyph(0, new_glyph.clone());
164 | assert!(result.is_ok());
165 |
166 | let result = font.get_glyph(0);
167 | assert_eq!(result.unwrap(), &new_glyph);
168 | }
169 |
170 | #[test]
171 | fn test_font_set_glyph_invalid_index() {
172 | let mut font = Font::default();
173 | let glyph = Glyph::default();
174 | let index = 256usize;
175 |
176 | let result = font.set_glyph(index, glyph);
177 | assert!(result.is_err());
178 | assert_matches!(
179 | result.unwrap_err(),
180 | CommonError::InvalidIndex { index: i, lenght: l } if i == index && l == font.lenght()
181 | );
182 | }
183 |
184 | #[test]
185 | fn test_font_iter() {
186 | let font = Font::default();
187 | let default_glyph = Glyph::default();
188 |
189 | for color in font.iter() {
190 | assert_eq!(color, &default_glyph);
191 | }
192 | }
193 |
194 | #[test]
195 | fn test_font_iter_mut() {
196 | let mut font = Font::default();
197 |
198 | let coord = Coord::new(0, 0);
199 | let mut new_glyph = Glyph::default();
200 | new_glyph.set_pixel(coord, GlyphPixel::Solid).unwrap();
201 |
202 | for glyph in font.iter_mut() {
203 | *glyph = new_glyph.clone();
204 | }
205 |
206 | for glyph in font.iter() {
207 | assert_eq!(glyph, &new_glyph);
208 | }
209 | }
210 |
211 | #[test]
212 | fn test_font_debug() {
213 | let font = Font::default();
214 | let data: Vec<&Glyph> = font.glyphs.iter().collect();
215 |
216 | let expected = format!("Font {{ data: {:?} }}", data);
217 | let result = format!("{:?}", font);
218 |
219 | assert_eq!(result, expected);
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/tinlib/src/machine/screen.rs:
--------------------------------------------------------------------------------
1 | //! Screen implementation and manipulation.
2 | use std::fmt;
3 | use std::slice;
4 |
5 | use crate::common::{
6 | CommonError, Coord, CoordEnumerate, CoordEnumerateMut, CoordIter, Result, Size,
7 | };
8 | use crate::graphic::Color;
9 |
10 | /// Screen width in pixels.
11 | const SCREEN_WIDTH: usize = 640;
12 | /// Screen width in pixels.
13 | const SCREEN_HEIGHT: usize = 384;
14 |
15 | /// A screen pixel or color.
16 | pub type ScreenPixel = Color;
17 | /// A iterator over all screen pixels.
18 | pub type ScreenPixelIter<'iter> = slice::Iter<'iter, ScreenPixel>;
19 | /// A mutable iterator over all screen pixels.
20 | pub type ScreenPixelIterMut<'iter> = slice::IterMut<'iter, ScreenPixel>;
21 | /// A enumeration iterator over all screen pixels and their coords.
22 | pub type ScreenPixelEnumerate<'iter> = CoordEnumerate<'iter, ScreenPixel>;
23 | /// A mutable enumeration iterator over all screen pixels and their coords.
24 | pub type ScreenPixelEnumerateMut<'iter> = CoordEnumerateMut<'iter, ScreenPixel>;
25 |
26 | /// A Screen representation with 640x384 tiles.
27 | pub struct Screen {
28 | pixels: [Color; SCREEN_WIDTH * SCREEN_HEIGHT],
29 | }
30 |
31 | impl Screen {
32 | /// Returns the width.
33 | pub fn width(&self) -> usize {
34 | SCREEN_WIDTH
35 | }
36 |
37 | /// Returns the height.
38 | pub fn height(&self) -> usize {
39 | SCREEN_HEIGHT
40 | }
41 |
42 | /// Returns the size.
43 | pub fn size(&self) -> Size {
44 | Size::new(self.width(), self.height())
45 | }
46 |
47 | /// Returns a pixel.
48 | pub fn get_pixel(&self, coord: Coord) -> Result {
49 | if !self.is_coord_valid(coord) {
50 | return Err(CommonError::new_invalid_coord(coord, self.size()));
51 | }
52 |
53 | let index = self.get_index(coord);
54 | Ok(self.pixels[index])
55 | }
56 |
57 | /// Sets a pixels.
58 | pub fn set_pixel(&mut self, coord: Coord, pixel: ScreenPixel) -> Result<()> {
59 | if !self.is_coord_valid(coord) {
60 | return Err(CommonError::new_invalid_coord(coord, self.size()));
61 | }
62 |
63 | let index = self.get_index(coord);
64 | self.pixels[index] = pixel;
65 |
66 | Ok(())
67 | }
68 |
69 | /// Clears all pixels to black.
70 | pub fn clear(&mut self) {
71 | for pixel in self.pixels.iter_mut() {
72 | *pixel = ScreenPixel::default();
73 | }
74 | }
75 |
76 | /// Returns an iterator over all screen coords.
77 | pub fn coords(&self) -> CoordIter {
78 | CoordIter::new(self.size())
79 | }
80 |
81 | /// Returns an iterator over all screen pixels.
82 | pub fn iter(&self) -> ScreenPixelIter {
83 | self.pixels.iter()
84 | }
85 |
86 | /// Returns a mutable iterator over all screen pixels.
87 | pub fn iter_mut(&mut self) -> ScreenPixelIterMut {
88 | self.pixels.iter_mut()
89 | }
90 |
91 | /// Returns an enumerate iterator over all screen pixels and tiles.
92 | pub fn enumerate(&self) -> ScreenPixelEnumerate {
93 | ScreenPixelEnumerate::new(self.coords(), self.iter())
94 | }
95 |
96 | /// Returns a mutable enumerate iterator over all screen pixels and tiles.
97 | pub fn enumerate_mut(&mut self) -> ScreenPixelEnumerateMut {
98 | ScreenPixelEnumerateMut::new(self.coords(), self.iter_mut())
99 | }
100 |
101 | fn is_coord_valid(&self, coord: Coord) -> bool {
102 | coord.x < self.width() && coord.y < self.height()
103 | }
104 |
105 | fn get_index(&self, coord: Coord) -> usize {
106 | coord.x * self.width() + coord.y
107 | }
108 | }
109 |
110 | impl Default for Screen {
111 | /// Creates a new black Screen.
112 | fn default() -> Self {
113 | Self {
114 | pixels: [Color::default(); SCREEN_WIDTH * SCREEN_HEIGHT],
115 | }
116 | }
117 | }
118 |
119 | impl fmt::Debug for Screen {
120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121 | let pixels: Vec<&ScreenPixel> = self.pixels.iter().collect();
122 |
123 | f.debug_struct("Screen").field("pixels", &pixels).finish()
124 | }
125 | }
126 |
127 | #[cfg(test)]
128 | mod tests {
129 | use assert_matches::assert_matches;
130 |
131 | use super::*;
132 |
133 | #[test]
134 | fn test_screen_default() {
135 | let screen = Screen::default();
136 | let default_pixel = ScreenPixel::default();
137 |
138 | assert_eq!(screen.pixels.len(), SCREEN_WIDTH * SCREEN_HEIGHT);
139 | assert!(screen.pixels.iter().all(|p| *p == default_pixel));
140 | }
141 |
142 | #[test]
143 | fn test_screen_width_height_and_size() {
144 | let screen = Screen::default();
145 |
146 | assert_eq!(screen.width(), SCREEN_WIDTH);
147 | assert_eq!(screen.height(), SCREEN_HEIGHT);
148 | assert_eq!(screen.size(), Size::new(SCREEN_WIDTH, SCREEN_HEIGHT));
149 | }
150 |
151 | #[test]
152 | fn test_screen_get_pixel() {
153 | let screen = Screen::default();
154 | let coord = Coord::new(1, 1);
155 | let color = Color::default();
156 |
157 | let result = screen.get_pixel(coord);
158 | assert!(result.is_ok());
159 | assert_eq!(result.unwrap(), color);
160 | }
161 |
162 | #[test]
163 | fn test_screen_get_pixel_invalid_coord() {
164 | let screen = Screen::default();
165 | let coord = Coord::new(641, 1);
166 |
167 | let result = screen.get_pixel(coord);
168 | assert!(result.is_err());
169 | assert_matches!(
170 | result.unwrap_err(),
171 | CommonError::InvalidCoord { coord: c, size: s } if c == coord && s == screen.size()
172 | );
173 | }
174 |
175 | #[test]
176 | fn test_screen_set_pixel() {
177 | let mut screen = Screen::default();
178 | let coord = Coord::new(1, 1);
179 | let pixel = ScreenPixel::new(255, 255, 255);
180 |
181 | let result = screen.set_pixel(coord, pixel);
182 | assert!(result.is_ok());
183 |
184 | let result = screen.get_pixel(coord);
185 | assert!(result.is_ok());
186 | assert_eq!(result.unwrap(), pixel);
187 | }
188 |
189 | #[test]
190 | fn test_screen_set_pixel_invalid_coord() {
191 | let mut screen = Screen::default();
192 | let coord = Coord::new(641, 1);
193 | let pixel = ScreenPixel::new(255, 255, 255);
194 |
195 | let result = screen.set_pixel(coord, pixel);
196 | assert!(result.is_err());
197 | assert_matches!(
198 | result.unwrap_err(),
199 | CommonError::InvalidCoord { coord: c, size: s } if c == coord && s == screen.size()
200 | );
201 | }
202 |
203 | #[test]
204 | fn test_screen_coords() {
205 | let screen = Screen::default();
206 |
207 | let mut x = 0usize;
208 | let mut y = 0usize;
209 | for coord in screen.coords() {
210 | assert_eq!(coord.x, x);
211 | assert_eq!(coord.y, y);
212 |
213 | y += 1;
214 | if y == screen.width() {
215 | y = 0;
216 | x += 1;
217 | }
218 | }
219 | }
220 |
221 | #[test]
222 | fn test_screen_iter() {
223 | let screen = Screen::default();
224 | let default_pixel = ScreenPixel::default();
225 |
226 | for pixel in screen.iter() {
227 | assert_eq!(pixel, &default_pixel);
228 | }
229 | }
230 |
231 | #[test]
232 | fn test_screen_iter_mut() {
233 | let mut screen = Screen::default();
234 | let new_pixel = ScreenPixel::new(255, 255, 255);
235 |
236 | for pixel in screen.iter_mut() {
237 | *pixel = new_pixel;
238 | }
239 |
240 | for pixel in screen.iter() {
241 | assert_eq!(pixel, &new_pixel);
242 | }
243 | }
244 |
245 | #[test]
246 | fn test_screen_enumerate() {
247 | let screen = Screen::default();
248 | let mut coorditer = screen.coords();
249 | let mut pixeliter = screen.iter();
250 |
251 | for (coord, pixel) in screen.enumerate() {
252 | assert_eq!(coord, coorditer.next().unwrap());
253 | assert_eq!(pixel, pixeliter.next().unwrap());
254 | }
255 | }
256 |
257 | #[test]
258 | fn test_screen_enumerate_mut() {
259 | let mut screen = Screen::default();
260 | let mut coorditer = screen.coords();
261 | let old_pixel = ScreenPixel::default();
262 | let new_pixel = ScreenPixel::new(255, 255, 255);
263 |
264 | for (coord, pixel) in screen.enumerate_mut() {
265 | assert_eq!(coord, coorditer.next().unwrap());
266 | assert_eq!(pixel, &old_pixel);
267 |
268 | *pixel = new_pixel;
269 | }
270 |
271 | for pixel in screen.iter() {
272 | assert_eq!(pixel, &new_pixel);
273 | }
274 | }
275 |
276 | #[test]
277 | fn test_screen_debug() {
278 | let screen = Screen::default();
279 | let data: Vec<&ScreenPixel> = screen.pixels.iter().collect();
280 |
281 | let expected = format!("Screen {{ pixels: {:?} }}", data);
282 | let result = format!("{:?}", screen);
283 |
284 | assert_eq!(result, expected);
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/tinlib/src/graphic/glyph.rs:
--------------------------------------------------------------------------------
1 | //! Glyph implementation and manipulation.
2 | use std::fmt;
3 | use std::slice;
4 |
5 | use crate::common::{
6 | CommonError, Coord, CoordEnumerate, CoordEnumerateMut, CoordIter, Result, Size,
7 | };
8 |
9 | /// The default Glyph width.
10 | pub const GLYPH_WIDTH: usize = 16;
11 | /// The default Glyph height.
12 | pub const GLYPH_HEIGHT: usize = 16;
13 |
14 | /// A Glyph pixel representation.
15 | #[derive(Debug, Clone, Copy, PartialEq)]
16 | pub enum GlyphPixel {
17 | /// An empty or transparent pixel.
18 | Empty,
19 | /// An solid pixel.
20 | Solid,
21 | }
22 |
23 | /// A iterator over all glyph pìxels.
24 | pub type GlyphPixelIter<'iter> = slice::Iter<'iter, GlyphPixel>;
25 | /// A mutable iterator over all glyph pìxels.
26 | pub type GlyphPixelIterMut<'iter> = slice::IterMut<'iter, GlyphPixel>;
27 | /// A enumeration iterator over all glyph pixels and their coords.
28 | pub type GlyphPixelEnumerate<'iter> = CoordEnumerate<'iter, GlyphPixel>;
29 | /// A mutable enumeration iterator over all glyph pixels and their coords.
30 | pub type GlyphPixelEnumerateMut<'iter> = CoordEnumerateMut<'iter, GlyphPixel>;
31 |
32 | /// A Glyph representation with NxM Pixels.
33 | #[derive(Clone)]
34 | pub struct Glyph {
35 | size: Size,
36 | data: Vec,
37 | }
38 |
39 | impl Glyph {
40 | /// Creates a new Glyph.
41 | pub fn new(size: Size) -> Self {
42 | Self {
43 | size,
44 | data: vec![GlyphPixel::Empty; size.width() * size.height()],
45 | }
46 | }
47 |
48 | /// Returns a Size.
49 | pub fn size(&self) -> Size {
50 | self.size
51 | }
52 |
53 | /// Returns a pixel.
54 | pub fn get_pixel(&self, coord: Coord) -> Result {
55 | if !self.is_coord_valid(coord) {
56 | return Err(CommonError::new_invalid_coord(coord, self.size));
57 | }
58 |
59 | let index = self.get_index(coord);
60 | Ok(self.data[index])
61 | }
62 |
63 | /// Sets a pixel.
64 | pub fn set_pixel(&mut self, coord: Coord, value: GlyphPixel) -> Result<()> {
65 | if !self.is_coord_valid(coord) {
66 | return Err(CommonError::new_invalid_coord(coord, self.size));
67 | }
68 |
69 | let index = self.get_index(coord);
70 | self.data[index] = value;
71 |
72 | Ok(())
73 | }
74 |
75 | /// Returns a iterator over the glyph's coords.
76 | pub fn coords(&self) -> CoordIter {
77 | CoordIter::new(self.size())
78 | }
79 |
80 | /// Returns an iterator over all Glyph pixels.
81 | pub fn iter(&self) -> GlyphPixelIter {
82 | self.data.iter()
83 | }
84 |
85 | /// Returns a mutable iterator over all Glyph pixels.
86 | pub fn iter_mut(&mut self) -> GlyphPixelIterMut {
87 | self.data.iter_mut()
88 | }
89 |
90 | /// Returns an enumerate iterator over glyph's coords and pixels.
91 | pub fn enumerate(&self) -> GlyphPixelEnumerate {
92 | GlyphPixelEnumerate::new(self.coords(), self.iter())
93 | }
94 |
95 | /// Returns a mutable enumerate iterator over glyph's coords and pixels.
96 | pub fn enumerate_mut(&mut self) -> GlyphPixelEnumerateMut {
97 | GlyphPixelEnumerateMut::new(self.coords(), self.iter_mut())
98 | }
99 |
100 | fn is_coord_valid(&self, coord: Coord) -> bool {
101 | coord.x < self.size.width() && coord.y < self.size.height()
102 | }
103 |
104 | fn get_index(&self, coord: Coord) -> usize {
105 | coord.x * self.size.width() + coord.y
106 | }
107 | }
108 |
109 | impl Default for Glyph {
110 | /// Creates a Glyph with all pixels black.
111 | fn default() -> Self {
112 | Self {
113 | size: Size::new(GLYPH_WIDTH, GLYPH_HEIGHT),
114 | data: vec![GlyphPixel::Empty; GLYPH_WIDTH * GLYPH_HEIGHT],
115 | }
116 | }
117 | }
118 |
119 | impl PartialEq for Glyph {
120 | fn eq(&self, other: &Self) -> bool {
121 | self.data.iter().zip(other.data.iter()).all(|(a, b)| a == b)
122 | }
123 | }
124 |
125 | impl fmt::Debug for Glyph {
126 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127 | let data: Vec<&GlyphPixel> = self.data.iter().collect();
128 |
129 | f.debug_struct("Glyph").field("data", &data).finish()
130 | }
131 | }
132 |
133 | #[cfg(test)]
134 | mod tests {
135 | use assert_matches::assert_matches;
136 |
137 | use super::*;
138 |
139 | #[test]
140 | fn test_glyph_default() {
141 | let glyph = Glyph::default();
142 |
143 | assert_eq!(glyph.data.len(), GLYPH_WIDTH * GLYPH_HEIGHT);
144 | for pixel in glyph.data.iter() {
145 | assert_eq!(*pixel, GlyphPixel::Empty);
146 | }
147 | }
148 |
149 | #[test]
150 | fn test_glyph_size() {
151 | let glyph = Glyph::default();
152 | assert_eq!(glyph.size(), Size::new(GLYPH_WIDTH, GLYPH_HEIGHT));
153 | }
154 |
155 | #[test]
156 | fn test_glyph_get_pixel() {
157 | let coord = Coord::new(1, 1);
158 | let glyph = Glyph::default();
159 |
160 | let result = glyph.get_pixel(coord);
161 | assert!(result.is_ok());
162 | assert_eq!(result.unwrap(), GlyphPixel::Empty);
163 | }
164 |
165 | #[test]
166 | fn test_glyph_get_pixel_invalid_coord() {
167 | let coord = Coord::new(17, 1);
168 | let glyph = Glyph::default();
169 |
170 | let result = glyph.get_pixel(coord);
171 | assert!(result.is_err());
172 | assert_matches!(
173 | result.unwrap_err(),
174 | CommonError::InvalidCoord { coord: c, size: s } if c == coord && s == glyph.size()
175 | );
176 | }
177 |
178 | #[test]
179 | fn test_glyph_set_pixel() {
180 | let coord = Coord::new(1, 1);
181 | let mut glyph = Glyph::default();
182 |
183 | let result = glyph.set_pixel(coord, GlyphPixel::Solid);
184 | assert!(result.is_ok());
185 |
186 | let result = glyph.get_pixel(coord);
187 | assert!(result.is_ok());
188 | assert_eq!(result.unwrap(), GlyphPixel::Solid);
189 | }
190 |
191 | #[test]
192 | fn test_glyph_set_pixel_invalid_coord() {
193 | let coord = Coord::new(17, 1);
194 | let mut glyph = Glyph::default();
195 |
196 | let result = glyph.set_pixel(coord, GlyphPixel::Solid);
197 | assert!(result.is_err());
198 | assert_matches!(
199 | result.unwrap_err(),
200 | CommonError::InvalidCoord { coord: c, size: s } if c == coord && s == glyph.size()
201 | );
202 | }
203 |
204 | #[test]
205 | fn test_glyph_coords() {
206 | let glyph = Glyph::default();
207 |
208 | let mut x = 0usize;
209 | let mut y = 0usize;
210 | for coord in glyph.coords() {
211 | assert_eq!(coord.x, x);
212 | assert_eq!(coord.y, y);
213 |
214 | y += 1;
215 | if y == glyph.size().width() {
216 | y = 0;
217 | x += 1;
218 | }
219 | }
220 | }
221 |
222 | #[test]
223 | fn test_glyph_iter() {
224 | let glyph = Glyph::default();
225 | let default_pixel = GlyphPixel::Empty;
226 |
227 | for pixel in glyph.iter() {
228 | assert_eq!(pixel, &default_pixel);
229 | }
230 | }
231 |
232 | #[test]
233 | fn test_glyph_iter_mut() {
234 | let mut glyph = Glyph::default();
235 | let new_pixel = GlyphPixel::Solid;
236 |
237 | for pixel in glyph.iter_mut() {
238 | *pixel = new_pixel;
239 | }
240 |
241 | for pixel in glyph.iter() {
242 | assert_eq!(pixel, &new_pixel);
243 | }
244 | }
245 |
246 | #[test]
247 | fn test_glyph_enumerate() {
248 | let glyph = Glyph::default();
249 | let mut coorditer = glyph.coords();
250 | let mut pixeliter = glyph.iter();
251 |
252 | for (coord, pixel) in glyph.enumerate() {
253 | assert_eq!(coord, coorditer.next().unwrap());
254 | assert_eq!(pixel, pixeliter.next().unwrap());
255 | }
256 | }
257 |
258 | #[test]
259 | fn test_glyph_enumerate_mut() {
260 | let mut glyph = Glyph::default();
261 | let mut coorditer = glyph.coords();
262 | let old_pixel = GlyphPixel::Empty;
263 | let new_pixel = GlyphPixel::Solid;
264 |
265 | for (coord, pixel) in glyph.enumerate_mut() {
266 | assert_eq!(coord, coorditer.next().unwrap());
267 | assert_eq!(pixel, &old_pixel);
268 |
269 | *pixel = new_pixel;
270 | }
271 |
272 | for pixel in glyph.iter() {
273 | assert_eq!(pixel, &new_pixel);
274 | }
275 | }
276 |
277 | #[test]
278 | fn test_glyph_partialeq() {
279 | let glyph_1 = Glyph::default();
280 | let mut glyph_2 = Glyph::default();
281 |
282 | assert_eq!(glyph_1, glyph_2);
283 |
284 | glyph_2.data[0] = GlyphPixel::Solid;
285 | assert_ne!(glyph_1, glyph_2);
286 | }
287 |
288 | #[test]
289 | fn test_glyph_debug() {
290 | let glyph = Glyph::default();
291 | let data: Vec<&GlyphPixel> = glyph.data.iter().collect();
292 |
293 | let expected = format!("Glyph {{ data: {:?} }}", data);
294 | let result = format!("{:?}", glyph);
295 |
296 | assert_eq!(result, expected);
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/tinlib/src/map/mod.rs:
--------------------------------------------------------------------------------
1 | //! Map utilities.
2 | use std::fmt;
3 | use std::slice;
4 |
5 | use crate::common::{
6 | CommonError, Coord, CoordEnumerate, CoordEnumerateMut, CoordIter, Result, Size,
7 | };
8 | use crate::graphic::{Color, Glyph};
9 |
10 | /// Map width in Glyphs.
11 | const MAP_WIDTH: usize = 320;
12 | /// Map height in Glyphs.
13 | const MAP_HEIGHT: usize = 192;
14 |
15 | /// A Tile representation with a glyph and a color.
16 | #[derive(Clone, Copy, PartialEq)]
17 | pub struct Tile<'refs> {
18 | /// A reference to a Glyph.
19 | pub glyph: &'refs Glyph,
20 | /// A reference to a Color.
21 | pub color: &'refs Color,
22 | }
23 |
24 | impl<'refs> Tile<'refs> {
25 | /// Creates a new Tile with references to a Glyph and a Color.
26 | pub fn new(glyph: &'refs Glyph, color: &'refs Color) -> Self {
27 | Self { glyph, color }
28 | }
29 | }
30 |
31 | impl fmt::Debug for Tile<'_> {
32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33 | f.debug_struct("Tile")
34 | .field("glyph", self.glyph)
35 | .field("color", self.color)
36 | .finish()
37 | }
38 | }
39 |
40 | /// A iterator over all map tiles.
41 | pub type MapTileIter<'iter, 'tile> = slice::Iter<'iter, Option>>;
42 | /// A mutable iterator over all map tiles.
43 | pub type MapTileIterMut<'iter, 'tile> = slice::IterMut<'iter, Option>>;
44 | /// A enumeration iterator over all map tiles and their coords.
45 | pub type MapTileEnumerate<'iter, 'tile> = CoordEnumerate<'iter, Option>>;
46 | /// A mutable enumeration iterator over all map tiles and their coords.
47 | pub type MapTileEnumerateMut<'iter, 'tile> = CoordEnumerateMut<'iter, Option>>;
48 |
49 | /// A Map representation with 320x192 tiles.
50 | pub struct Map<'tile> {
51 | /// Map's tiles.
52 | pub tiles: [Option>; MAP_WIDTH * MAP_HEIGHT],
53 | }
54 |
55 | impl<'tile> Map<'tile> {
56 | /// Returns the width.
57 | pub fn width(&self) -> usize {
58 | MAP_WIDTH
59 | }
60 |
61 | /// Returns the height.
62 | pub fn height(&self) -> usize {
63 | MAP_HEIGHT
64 | }
65 |
66 | /// Returns the size.
67 | pub fn size(&self) -> Size {
68 | Size::new(self.width(), self.height())
69 | }
70 |
71 | /// Returns a tile.
72 | pub fn get_tile(&self, coord: Coord) -> Result