What's the best way to implement positioning functionality in my Tetris project? Traits or structs?

I'm trying to build Tetris in Rust. I have this struct for Tetrominoes whose functionality I want to split into different parts.

So, to me, a Tetromino at its core is just an array of 4 coordinates, and that's how I represent it in my code.

struct Tetromino {
	minoes: [Mino; 4]
}

struct Mino {
	x_to_center: HalfStep,
	y_to_center: HalfStep
}

/* HalfStep is a numerical type I made that rounds itself to the nearest half. */

But, a Tetromino isn't just a bunch of coordinates; it has to move too! My original solution was to add another field to my Tetromino struct to give it a position, then subtract from the y coordinate of that position to make it slowly fall to the bottom of the screen.

struct Tetromino {
	minoes: [Mino; 4],
	center: OnGrid
}

impl Tetromino {
	fn fall(mut self, speed: f32, delta_time: Duration) -> Self {
		self.center.row -= speed * delta_time.as_secs_f32();
		self
	}

	fn snap_to_grid(&self) -> [Snapped; 4] {
		self.minoes.map(|mino| Snapped {
			row: (
				self.center.row + f32::from(mino.y_to_center)
			).floor() as i8,
			column: (
				f32::from(self.center.column) +
				f32::from(mino.x_to_center)
			).floor() as i8,
		})
	}
}

struct OnGrid {
	row: f32,
	column: HalfStep
}

struct Snapped {
	row: i8,
	column: i8,
}

Pretty standard stuff so far, and my original solution served me well. Now, here's where my problem comes in.

So far, I've been using terminal graphics for my Tetris implementation, but now I want to upgrade to raylib. Now that I'm starting to switch to raylib, I've realized that it would be better if I separated my center field from my Tetromino struct.

Why? Because I don't just need to display Tetrominoes on the playfield grid, but I need to display them on the Next Queue and the Hold Queue too.

Though the Tetromino would need a position in each of those cases, they all need a different kind of position. For the playfield grid, I need the Tetromino's position to be in terms of rows and columns, but for the Next Queue, I need its position to be more in terms of pixels on the screen.

What I want to have is something like this:

struct Tetromino {
	minoes: [Mino; 4]
}

struct OnGrid {
	row:  f32,
	column: HalfStep
}

struct OnScreen {
	x: Pixels,
	y: Pixels
}

struct Game {
	falling_tetromino: Positioned<Tetromino, OnGrid>
}

struct Graphics {
	falling_tetromino: Positioned<Tetromino, OnScreen>
}

My question is, how do you best implement this "Positioned" thing?

Here's what I've tried so far.

struct Position<X: Numeric, Y: Numeric> {
	x: X,
	y: Y
}

struct Positioned<T, X: Numeric, Y: Numeric> {
	positioned: T,
	position: Position<X, Y> // Maybe rewrite OnGrid as OnGrid(Position<HalfStep, f32>) ???
}
trait Position {
	type X: Numeric;
	type Y: Numeric;

	fn x(&self) -> &Self::X;
	fn y(&self) -> &Self::Y;
	
	fn x_mut(&mut self) -> &mut Self::X;
	fn y_mut(&mut self) -> &mut Self::Y;
}

trait Positioned {
	type T;
	type P: Position;

	fn positioned(&self) -> &Self::T;
	fn position(&self) -> &Self::P;
}
trait Position {
	type X: Numeric;
	type Y: Numeric;

	fn x(&self) -> &Self::X;
	fn y(&self) -> &Self::Y;
	
	fn x_mut(&mut self) -> &mut Self::X;
	fn y_mut(&mut self) -> &mut Self::Y;
}

struct Positioned<T, P: Position> {
	positioned: T,
	position: P
}

I've tried these, but none of just them feel right. So now, I'm here, asking you guys for help.

I also want this kind of composability for other things like Rotation

let curently_falling_tetromino = Rotated { 
        rotated: Positioned { 
                positioned: Tetromino { minoes: /*  */ }
                position: OnGrid { row: 10, column: 10 }
        },
        rotation: Deg(90)
}

I'd also like to be able to access the Tetromino's fields from at the top level like:

/* I've made this work using the Deref trait, 
    but I don't think that's how I'm supposed to do it. */

println!("{:#?}", curently_falling_tetromino.minoes); // Could we make this work too?
``

If you want to combine two kinds of data without changing either of them, then the correct solution is composition.

Thanks for the reply : D

How would the code for that look like?
Maybe something like this?

struct ScreenPositionedTetromino {
        tetromino: Tetromino,
        position: ScreenPosition
}

struct GridPositionedTetromino {
        tetromino: Tetromino,
        position: GridPosition
}

struct ScreenPosition {
        x: Pixels,
        y: Pixels
}

struct GridPosition {
        row: f32,
        column: HalfStep
}

ScreenPositionedTetromino and GridPositionedTetromino are different types that both own a Tetromino. If this is adequate for your needs then yes, it is the right way to do it! From the OP, it sounds like it would fit. Just needs some methods to convert between the two.

3 Likes

Actually, yeah, now that I think about it, It doesn't really need to be generic.
Thanks for the help!

You could also think about having all tetrominoes be grid-positioned but have multiple grids that they can be attached to: one for the playfield, one for the next piece display, etc. That keeps the concrete pixel-placement logic separate from the more abstract tetromino manipulation logic.

Huh, that's a pretty elegant solution. I'll think about it, but I'll try the approach I'm currently implementing first

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.