Board

As the name suggests, this smart contract relates to the Board where we can draw stuffs, so the implementation is whatever that the board logic has.

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

As usual, we use u16 instead of u32. And given that \( 50 \times 50 = 2500 < 65536 \) where \( 2^{16} = 65536 \) the max number for u16, hence we could use u16 here. If we have more pixels, we might prefer to use u32. Of course, for general implementation, you could just use u32 without thinking much about it, it's perfectly fine. One just want to show you that it still works with u16, if however it does optimize performance in any way.

Next, we look at the implementation of each Pixel:

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

We first define what to go into each Pixel. So each Pixel will have a color, and who owns the Pixel. Because it's drawing, of course we'll have a variety of color available for each pixel, or we won't know what is drawn. And color just makes the Pixel more beautiful; it doesn't affect its price, nor the pricing changes according to what color to draw. That could be implemented if required, but not in this game.

Then we have PixelLine.

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

Now this concept of PixelLine is very useful. In general, they're called Lines, which you can read more about it here.

Lines are something one never thought before so clearly until reading this. One can't remember where one read it (and can't even find the reference), but someone said that you could think of it as "levels" in game. Example you might play a game that passes levels, level 1, 2, 3, etc. The "lines" concept could be imagine as that;

Here our lines are just the "rows" in the board. Line 1 = row 1, line 2 = row 2, etc. Each row contains the metadata of the Pixel that contains within it. This allows easier coordination and handling of data. And since PixelLine is a vector of Pixel, we don't need to alternatively define the coordinates somewhere, provided this is 2D.

So, one isn't really sure why this implementation, but one hypothesize it saves data. Example: a LookupMap containing row and column stored somewhere, or perhaps each pixel changes it's struct to become:

pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
    pub coordinate: (u16, u16),  // row, column
}

might be less efficient? You are welcome to try to change the implementation and play with it, to see which is more optimized for example.

Then we have the board, here it's PixelBoard. We'll skip the struct definintion for now and talk about it later, as this is important. Let's first talk about SetPixelRequest. This function SetPixelRequest is called by the user to "rent" the particular pixel. The user can define the color of the pixel he want to set after renting, so that is required in the struct. Otherwise, the coordinates are also required.

use near_sdk::serde::{Deserialize, Serialize}

pub const BOARD_WIDTH: u32 = 50;
pub const BOARD_HEIGHT: u32 = 50;

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u16,
    pub y: u16,
    pub color: u32,
}

impl SetPixelRequest {
  pub fn assert_valid(&self) {
    assert!(self.x < BOARD_WIDTH, "X is out of bounds");
    assert!(self.y < BOARD_HEIGHT, "Y is out of bounds");
    assert!(self.color <= 0xffffff, "Color is out of bounds");
  }
}

Then let's come back to PixelBoard with its implementation:

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u32,
    pub y: u32,
    pub color: u32,
}

impl SetPixelRequest {
    pub fn assert_valid(&self) {
        assert!(self.x < BOARD_WIDTH, "X is out of bounds");
        assert!(self.x < BOARD_HEIGHT, "Y is out of bounds");
        assert!(self.color <= 0xffffff, "Color is out of bounds");
    }
}

// We rearranged a bit by moving PixelBoard down here, 
// supposedly it's defined before SetPixelRequest. 
#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

impl PixelBoard {
    pub fn new() -> Self {
      let mut board = Self {
        lines: Vector::new(b"p".to_vec()),
        line_versions: vec![0; BOARD_HEIGHT as usize],
      };

      let default_line = PixelLine::default();
      for _ in 0..BOARD_HEIGHT {
        board.lines.push(&default_line);
      }
      board
    }

    pub fn get_line(&self, index: u32) -> PixelLine {
      self.lines.get(u64::from(index)).unwrap()
    }

    /// returns the list of old owner IDs for the replaced pixels
    pub fn set_pixels(
      &mut self,
      new_owner_id: u32,
      pixels: &[SetPixelRequest],
    ) -> HashMap<AccountIndex, u32> {
      let mut lines = HashMap::new();
      let mut old_owners = HashMap::new();

      for request in pixels {
        request.assert_valid();
        let line = lines
            .entry(request.y)
            .or_insert_with(|| self.lines.get(u64::from(request.y)).unwrap())
        let old_owner = line.0[request.x as usize].owner_id;
        line.0[request.x as usize] = Pixel {
          owner_id: new_owner_id,
          color: request.color
        };
        *old_owners.entry(old_owner).or_default() += 1;
      }

      for (i, line) in lines {
        self.save_line(i, &line);
      }

      old_owners
    }

    fn save_line(&mut self, index: u32, line: &PixelLine) {
      self.lines.replace(u64::from(index), line);
      self.line_versions[index as usize] += 1;
    }
}

We have two arguments: lines and line_versions. lines is easy to understand, just the collection of PixelLines, stored as a vector. All existing PixelLines add up to a PixelBoard.

Then there's line_versions. We see this is being used in the save_line function. Checking it, this means that each PixelLine has a version number. Everytime that particular PixelLine has some pixels overridden, the line_versions increment by 1. One isn't quite sure why this is required, though. Perhaps we'll know when we read lib.rs, but based on board.rs, there isn't much information except one described above. The line passed in is the overwritten PixelLine. The line is changed within set_pixels.

We also have the implementation for Place on the board, aside from the implementation in lib.rs. This contains specific implementations that consult the board.

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u32,
    pub y: u32,
    pub color: u32,
}

impl SetPixelRequest {
    pub fn assert_valid(&self) {
        assert!(self.x < BOARD_WIDTH, "X is out of bounds");
        assert!(self.x < BOARD_HEIGHT, "Y is out of bounds");
        assert!(self.color <= 0xffffff, "Color is out of bounds");
    }
}


#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

impl PixelBoard {
    pub fn new() -> Self {
      let mut board = Self {
        lines: Vector::new(b"p".to_vec()),
        line_versions: vec![0; BOARD_HEIGHT as usize],
      };

      let default_line = PixelLine::default();
      for _ in 0..BOARD_HEIGHT {
        board.lines.push(&default_line);
      }
      board
    }

    pub fn get_line(&self, index: u32) -> PixelLine {
      self.lines.get(u64::from(index)).unwrap()
    }

    /// returns the list of old owner IDs for the replaced pixels
    pub fn set_pixels(
      &mut self,
      new_owner_id: u32,
      pixels: &[SetPixelRequest],
    ) -> HashMap<AccountIndex, u32> {
      let mut lines = HashMap::new();
      let mut old_owners = HashMap::new();

      for request in pixels {
        request.assert_valid();
        let line = lines
            .entry(request.y)
            .or_insert_with(|| self.lines.get(u64::from(request.y)).unwrap())
        let old_owner = line.0[request.x as usize].owner_id;
        line.0[request.x as usize] = Pixel {
          owner_id: new_owner_id,
          color: request.color
        };
        *old_owners.entry(old_owner).or_default() += 1;
      }

      for (i, line) in lines {
        self.save_line(i, &line);
      }

      old_owners
    }

    fn save_line(&mut self, index: u32, line: &PixelLine) {
      self.lines.replace(u64::from(index), line);
      self.line_versions[index as usize] += 1;
    }
}

#[near_bindgen]
impl Place {
    pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> {
      lines
          .into_iter()
          .map(|i| {
            let line = self.board.get_line(i);
            line.try_to_vec().unwrap().into()
          })
          .collect()
    }

    pub fn get_line_versions(&self) -> Vec<u32> {
      self.board.line_versions.clone()
    }
}

These are view functions to get_lines and get_line_versions.

References