Rustでテトリス

  • 11
    いいね
  • 1
    コメント

テトリスとは

降り続くいくつかの四角を操作し、その地を満たすように並べていくと
自然と四角は消え、積もる四角は下へと落ちていく
それを繰り返せば、日頃の煩悩は薄れ、さらに続ければ悟りが開かれよう
それすなわちテトリスと言う

...ナンノコッチャ...

というわけで皆さんご存知テトリスです

以前はターミナルで動くテトリスをRubyで作りました

tetris.gif

今回は少しリッチにOpenGLで描写するテトリスをRustでやります
source:github

コード

まずは描写とは分離されたテトリスのロジック本体

lib.rs

lib.rs
extern crate rand;
use rand::distributions;
use rand::distributions::IndependentSample;

use std::f32;

struct Tetris

操作するブロックとブロックを配置するフィールドを状態として持ちます
structは基本let mutされて使うことを前提とします

lib.rs
pub struct Tetris {
  pub block: Block,
  pub field: [[Color; 10]; 20],
}

struct Block

ブロックはひとかたまり同じ色として扱います
それぞれのブロックのサイズが違うので配列ではなくベクターを使い表現します

lib.rs
pub struct Block {
  pub color: Color,
  pub blocks: Vec<(i32,i32)>,
}

enum Color

色は黒色をフィールド上ではブロック未配置とします
OptionのNoneをそれとして扱った方がよかったかもね
そのままではOpenGLの色情報として扱えないのであとで変換してあげます

lib.rs
#[derive(PartialEq, Copy, Clone)]
pub enum Color {
  Black, Red, Green, Blue, Yellow, Cyan, Magenta, White
}

enum Control

ブロック操作コマンド

lib.rs
#[derive(PartialEq)]
pub enum Control {
  Down, Left, Right, Rotate
}

const COLORS, BLOCKS

降ってくるブロック色と形と
ちなみにconstではVec::new()などのアロケーションは使えないようです、当たり前か

lib.rs
const COLORS: &'static [Color] = &[
  Color::Red,
  Color::Green,
  Color::Blue, 
  Color::Yellow, 
  Color::Cyan, 
  Color::Magenta,
];

const BLOCKS: &'static [&'static [(i32,i32)]] = &[ 
  &[(0,0),(0,1),(1,0),(1,1)],
  &[(0,0),(0,1),(0,2),(1,1),(2,1)],
  &[(0,0),(0,1),(0,2),(0,3)],
  &[(0,0),(0,1),(0,2),(0,3),(1,3)],
  &[(0,0),(0,1),(0,2),(0,3),(1,0)],
  &[(0,0),(0,1),(1,1),(1,2)],
  &[(1,0),(1,1),(0,1),(0,2)],
  &[(0,1),(0,1),(0,2),(1,1)],
];

impl Block, fn new, rand, rotate, down, left, right

まずはブロックの操作や初期化を実装
回転はもうちょっとなんとかなりそう

lib.rs
impl Block {
  pub fn new(c: Color, b: Vec<(i32,i32)>) -> Block {
    return Block {
      color: c,
      blocks: b
    };
  }

  pub fn rand() -> Block {
    let mut rng = rand::thread_rng();
    let blocks_range = distributions::Range::new(0, BLOCKS.len());
    let colors_range = distributions::Range::new(0, COLORS.len());
    return Block {
      color: COLORS[colors_range.ind_sample(&mut rng)],
      blocks: BLOCKS[blocks_range.ind_sample(&mut rng)].to_vec()
    };
  }

  fn down(&mut self) {
    for c in self.blocks.iter_mut() {
      c.0 += 1;
    }
  }

  fn left(&mut self) {
    for c in self.blocks.iter_mut() {
      c.1 -= 1;
    }
  }

  fn right(&mut self) {
    for c in self.blocks.iter_mut() {
      c.1 += 1;
    }
  }

  fn rotate(&mut self) {
    let r: f32 = f32::consts::PI / 2.0;
    let cy: f32 = 
      (self.blocks.iter().map(|i| i.0).sum::<i32>() as f32) / (self.blocks.len() as f32);
    let cx: f32 = 
      (self.blocks.iter().map(|i| i.1).sum::<i32>() as f32) / (self.blocks.len() as f32);

    for c in self.blocks.iter_mut() {
      let (y, x) = *c;
      let y = f32::from(y as i16);
      let x = f32::from(x as i16);
      *c = (
        (cy + (x - cx) * r.sin() + (y - cy) * r.cos()).round() as i32,
        (cx + (x - cx) * r.cos() - (y - cy) * r.sin()).round() as i32
      );
    }
  }
}

impl Tetris, fn new, control, delete, fall

fn controlがブロックがフィールド上で移動可能どうか、可能なら移動させる
fn deleteは行が満ちていれば消し、フィールド上のブロックを落下させる
fn fallが現在操作中のブロックを1マス下へ落とす
もう落とせなければブロックをフィールドへ書き込み、新しいブロックを設定する

lib.rs
impl Tetris {
  pub fn new() -> Tetris {
    return Tetris {
      block: Block::rand(),
      field: [[Color::Black; 10]; 20],
    };
  }

  pub fn control(&mut self, op: Control) {
    let pre = self.block.blocks.clone();
    match op {
      Control::Down => self.block.down(),
      Control::Left => self.block.left(),
      Control::Right => self.block.right(),
      Control::Rotate => self.block.rotate()
    }

    let ly = self.field.len() as i32;
    let lx = self.field[0].len() as i32;
    let exists = self.block.blocks.iter().all(|&(y,x)| {
      return 0 <= y && y < ly && 0 <= x && x < lx 
        && (self.field[y as usize][x as usize] == Color::Black);
    });

    if !exists {
      self.block.blocks = pre;
    }
  }

  pub fn delete(&mut self) {
    for y in 0 .. self.field.len() {
      if self.field[y].iter().all(|c| *c != Color::Black) {
        for x in 0 .. self.field[y].len() {
          let mut yy = y;
          for yyy in (0 .. y - 1).rev() {
            self.field[yy][x] = self.field[yyy][x];
            yy -= 1;
          }
        }
      }
    }
  }

  pub fn fall(&mut self) {
    let blocks = self.block.blocks.clone();
    self.control(Control::Down);

    let mut not_moved = true;
    let len = self.block.blocks.len();
    for i in 0 .. len {
      if self.block.blocks[i] != blocks[i] {
        not_moved = false; 
        break;
      }
    }

    if not_moved {
      {
        let ref bs = self.block.blocks;
        for &(y,x) in bs {
          self.field[y as usize][x as usize] = self.block.color;
        }
      }
      self.block = Block::rand();
      self.delete();
    }
  }
}

main.rs

続いて、描写部分
gliumクレートを使います

main.rs
#[macro_use]
extern crate glium;
use glium::{DisplayBuild, Surface, Program};
use glium::{glutin, index, vertex};

extern crate tetris;
use tetris::{Tetris, Color};

use std::f32;
use std::thread;
use std::time;
use std::sync::{Arc, Mutex};

struct Vertex

gliumでは自前でVertexなstructを用意しなければなりません
が、マクロがトレイトを実装してくれるので楽チンです

main.rs
#[derive(Copy, Clone)]
struct Vertex {
  pos: [f32; 2],
  color: [f32; 4],
}
implement_vertex!(Vertex, pos, color);

const VERTEX_SHADER, FRAGMENT_SHADER

シェーダーも必須です
バーテックス経由でテトリスのフィールド情報を渡しています
ここもうちょっと上手いことできないかな

main.rs
const VERTEX_SHADER: &'static str = r#"
#version 400

in vec2 pos;
in vec4 color;
out vec4 v_color;

void main() {
  v_color = color;
  gl_Position = vec4(pos, 0, 1);
}
"#;

const FRAGMENT_SHADER: &'static str = r#"
#version 400

in vec4 v_color;
out vec4 f_color;

void main() {
  f_color = v_color;
}
"#;

fn color_to_rgba

RustのenumをOpenGLの色vec4へ変換

main.rs
pub fn color_to_rgba(c: Color) -> [f32; 4] {
  match c {
    Color::Black   => [0.0, 0.0, 0.0, 0.0],
    Color::Red     => [0.5, 0.0, 0.0, 0.5],
    Color::Green   => [0.0, 0.5, 0.0, 0.5],
    Color::Blue    => [0.0, 0.0, 0.5, 0.5],
    Color::Yellow  => [0.5, 0.5, 0.0, 0.5],
    Color::Cyan    => [0.0, 0.5, 0.5, 0.5],
    Color::Magenta => [0.5, 0.0, 0.5, 0.5],
    Color::White   => [0.5, 0.5, 0.5, 0.5]
  }
}

fn tetris_to_vertexs

テトリスの状態をバーテックスシェーダーへ渡す情報へ変換
本当はスケールなりトランスレートなりで表示位置変えたいのだけど
シェーダーだけでどうやるのかわからなかった
glScaleとかglTranslateとかをgliumが提供していないから、nalgebraの行列関数使えってことなのかな
デフォルトの-1 .. 1の間でどうにか表現、めんどかった

main.rs
fn tetris_to_vertexs(tetris: &Tetris) -> Vec<Vertex> {
  let mut vs = vec!();
  let mut y: f32 = 0.80;
  let mut iy: usize = 0;

  while y >= -0.885 {
    let mut x: f32 = -0.375;
    let mut ix: usize = 0;

    while x <= 0.46  {
      if tetris.block.blocks.iter().any(|&(yy,xx)| iy as i32 == yy && ix as i32 == xx) {
        vs.push(Vertex { 
          pos: [x, y], 
          color: color_to_rgba(tetris.block.color)
        });
      }
      else {
        vs.push(Vertex { 
          pos: [x, y], 
          color: color_to_rgba(tetris.field[iy][ix])
        });
      }

      x += 0.085;
      ix += 1;
    }

    y -= 0.085;
    iy += 1;
  }
  return vs;
}

fn main

やっとメイン
スレッドでタイマーを回し、メインスレッドでキー入力と表示を捌く
バーテックスとインデックスとシェーダーとユニフォームとパラメーターを用意してドロー!

実は排他制御が甘いせいか、mutのゴリ押しのせいか、表示がたまに崩れます
解決策がわかりませんでした😇

main.rs
fn main() {
  let display = glutin::WindowBuilder::new()
    .with_dimensions(600, 600).build_glium().unwrap();
  println!("{:?}", display. get_framebuffer_dimensions());

  let index = index::NoIndices(index::PrimitiveType::Points);
  let uniform = uniform!();
  let param = glium::DrawParameters { 
    point_size: Some(26.0),
    .. Default::default()
  };

  let program = match Program::from_source(&display, VERTEX_SHADER, FRAGMENT_SHADER, None) {
    Ok(p) => p,
    Err(e) => {
      println!("{}", e);
      return;
    }
  };

  let mutex_main = Arc::new(Mutex::new(Tetris::new()));
  let mutex_timer = mutex_main.clone();

  thread::spawn(move || {
    loop {
      thread::sleep(time::Duration::from_millis(1000));
      let mut t = mutex_timer.lock().unwrap();
      t.fall();
    }
  });

  loop {
    let mut t = mutex_main.lock().unwrap();

    'event: for e in display.poll_events() {
      match e {
        glutin::Event::KeyboardInput(
          glutin::ElementState::Pressed, _, Some(keycode)
        ) => {
          //println!("{:?}", e);
          match keycode {
            glutin::VirtualKeyCode::Up => {
              t.control(tetris::Control::Rotate);
            },

            glutin::VirtualKeyCode::Down => {
              t.fall();
            },

            glutin::VirtualKeyCode::Left => {
              t.control(tetris::Control::Left);
            },

            glutin::VirtualKeyCode::Right => {
              t.control(tetris::Control::Right);
            },

            glutin::VirtualKeyCode::Q => {
              return
            },

            _ => {}
          }

          break 'event;
        },

        _ => break 'event
      }
    }

    let mut frame = display.draw();

    let vertex_buffer = vertex::VertexBuffer::new(
       &display, &tetris_to_vertexs(&t) 
     ).unwrap();

    frame.clear_color(0.0, 0.0, 0.0 ,0.0);
    frame.draw(&vertex_buffer, &index, &program, &uniform, &param).unwrap();
    frame.finish().unwrap();
  } 
}

こいつ! 動くぞ!!

tetris.gif
(あぁ 表示崩れてる)

結び

ようやくRustと仲良く慣れた気がする
RcやRefCell、ArcやMutexなど
まだまだ理解が足りないところが多いが楽しくRust書けるようになってきたので良しとしよう