Session 10 (12.01.24) - RUB-INI-Theory-of-Machine-Learning/Info1_WS23-24 GitHub Wiki

In der zehnten Session implementieren wir das Zwei-Spieler-Spiel. Hierzu erstellen wir zunächst eine Game-Klasse in der wir die bisher im globalen Namespace liegenden Funktionen zusammenfassen (insbesondere Funktionen die über Events aufgerufen werden). Außerdem müssen wir dem Renderer einen virtuellen Canvas geben, damit dieser die Spiele an unterschiedliche Positionen des Canvas zeichnen kann.

Ziele

  • Game-Klasse
  • Virtueller Canvas
  • Zwei-Spieler-Spiel

Game-Klasse

class Game {
public:
    var base_score_values = [0, 40, 100, 300, 1200];
    var level_gravity = [ ... ];
    
    var level, lines, score, soft_drop_start, gravity, current_time, spawned;
    var pool, tetromino, playfield, renderer;
    
    constructor(){
        this.level = 9;
        this.lines = 0;
        this.score = 0;
        this.soft_drop_start = 0;
        this.gravity = level_gravity[this.level];
        this.current_time = time();
        this.spawned = false;
    
    
        this.pool = TetrominoPool();
        this.tetromino = Tetromino(this.pool.get(), Integer((playfield_size["x"] + 2)/2 - 2));
        this.playfield = Playfield(playfield_size["x"], playfield_size["y"]);
        this.renderer = Renderer(playfield_size["x"], playfield_size["y"]);
    }
    
    function step(){ ... }
    
    function keydown(key){ ... }
    
    function keyup(key){ ... }
}
var games = [Game()];
setEventHandler("timer", function(event) {
    for var game in games do
        game.step();
});
setEventHandler("canvas.keydown", function(event){
    for var game in games do
        game.keydown(event.key);
});
setEventHandler("canvas.keyup", function(event){
    for var game in games do
        game.keyup(event.key);
});

Virtueller Canvas

Übergeben der Parameter und Berechnung der Blockgröße

class Renderer {
public: 
    var playfield_width, playfield_height;
    var x, y, width, height;
    
    ...
    

public:
    constructor(playfield_width, playfield_height, x, y, width, height){
        this.playfield_width = playfield_width;
        this.playfield_height = playfield_height;
    
        this.x = math.floor(x);
        this.y = math.floor(y);
        this.width = math.floor(width);
        this.height = math.floor(height);
    
      
        this.block_size = math.floor(math.min(
            this.height/(playfield_size["y"]+2),
            this.width/(playfield_size["x"]+2 + this.sidebar_size)
        ));
    
       ...
    
    }
}

Aufruf im Constructor der Game-Klasse

class Game {
public:
    constructor(v_canvas = [0,0,0,0]) {
        if(v_canvas[2] == 0) then v_canvas[2] = canvas.width();
        if(v_canvas[3] == 0) then v_canvas[3] = canvas.height();
        
        this.renderer = Renderer(playfield_size["x"], playfield_size["y"], v_canvas[0], v_canvas[1], v_canvas[2], v_canvas[3]);
    }
}

Einsetzen von Versatz und Breite/Höhe

Exemplarisch die draw_text() Funktion:

function draw_text(text, row){
    canvas.setFillColor(0,0,0);
    canvas.fillRect(this.x + block_size * (this.playfield_width + 3) , this.y + row * block_size -1, 4 * block_size, 2 * block_size);

    canvas.setFillColor(1,1,1);
    canvas.text(this.x + block_size * (this.playfield_width + 3), this.y + row * block_size, text);
}

Weitere Anpassungen im constructor(), in draw_next_tetromino() und draw_block() notwendig. Diese sind komplett analog zu den hier exemplarisch gezeigten Einsetzungen.

Zwei-Spieler-Spiel

class Game {
public:
    var incoming_lines = 0;
    var outgoing_lines = 0;
    
    ...

    var bindings;
    
    constructor(v_canvas = [0,0,0,0], bindings=["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "]){
        this.bindings = bindings;
      
        ...
    
    }
    
    function step() {
    
        ...
            
        if this.incoming_lines > 0 then {
            this.playfield.insert_rows(this.incoming_lines);
            this.incoming_lines = 0;
        }
        
        var cleared_rows = this.playfield.remove_full_rows();
        if cleared_rows > 0 then {
            
            ...
            
            if cleared_rows < 4 then this.outgoing_lines += cleared_rows - 1;
            else this.outgoing_lines += 4;
        }
    }
    
    function keydown(key){
		this.playfield.remove_tetromino(this.tetromino);

		if key == this.bindings[4] then {
			if this.soft_drop_start == 0 then this.soft_drop_start = Integer(this.tetromino.row);
				this.gravity = 50;
			};
			if key == this.bindings[0] then this.tetromino.rotate_left();
			if key == this.bindings[1] then this.tetromino.rotate_right();
			if key == this.bindings[2] then this.tetromino.move_left();
			if key == this.bindings[3] then this.tetromino.move_right();
			if this.playfield.check_collision(this.tetromino) then {
				this.tetromino.undo(); # The action caused an invalid playfield state. Undo it. 
		}
    }
    
    function keyup(key){
		if key == this.bindings[4] and this.soft_drop_start != 0 then {
			this.soft_drop_start = 0;
			this.gravity = this.level_gravity[this.level];
		}
	}
}


var games = [
	Game(v_canvas=[0, 0, canvas.width(), canvas.height()/2]),
	Game(v_canvas=[0, canvas.height()/2, canvas.width(), canvas.height()/2], bindings=["8", "5", "4", "6", "0"])
	];
setEventHandler("timer", function(event) {
	for var game_idx in 0:games.size() do {
		var game = games[game_idx];
		if game.outgoing_lines > 0 then {
			games[(game_idx + 1) % games.size()].incoming_lines += game.outgoing_lines;
			game.outgoing_lines = 0;
		}
		try {
			game.step();
		}
		catch var e do {
			quitEventMode();
		}
	}
});