AndyObtiva/glimmer-dsl-libui

View on GitHub
examples/tetris/model/tetromino.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (c) 2021-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require_relative 'block'

require 'matrix'

class Tetris
  module Model
    class Tetromino
      ORIENTATIONS = [:north, :east, :south, :west]
      
      LETTER_COLORS = {
        I: :cyan,
        J: :blue,
        L: :olive,
        O: :yellow,
        S: :lime,
        T: :magenta,
        Z: :red,
      }
      
      attr_reader :game, :letter, :preview
      alias preview? preview
      attr_accessor :orientation, :blocks, :row, :column
      
      def initialize(game)
        @game = game
        @letter = LETTER_COLORS.keys.sample
        @orientation = :north
        @blocks = default_blocks
        @preview = true
        new_row = 0
        new_column = (Model::Game::PREVIEW_PLAYFIELD_WIDTH - width)/2
        update_playfield(new_row, new_column)
      end
      
      def playfield
        @preview ? game.preview_playfield : game.playfield
      end
      
      def launch!
        remove_from_playfield
        @preview = false
        new_row = 1 - height
        new_column = (game.playfield_width - width)/2
        update_playfield(new_row, new_column)
        game.tetrominoes << self
      end
      
      def update_playfield(new_row = nil, new_column = nil)
        remove_from_playfield
        if !new_row.nil? && !new_column.nil?
          @row = new_row
          @column = new_column
          add_to_playfield
        end
      end
      
      def add_to_playfield
        update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
          playfield[playfield_row][playfield_column].color = blocks[row_index][column_index].color if playfield_row >= 0 && playfield[playfield_row][playfield_column]&.clear? && !blocks[row_index][column_index].clear? && playfield[playfield_row][playfield_column].color != blocks[row_index][column_index].color
        end
      end
      
      def remove_from_playfield
        return if @row.nil? || @column.nil?
        update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
          playfield[playfield_row][playfield_column].clear if playfield_row >= 0 && !blocks[row_index][column_index].clear? && playfield[playfield_row][playfield_column]&.color == color
        end
      end
      
      def stopped?
        return true if @stopped || @preview
        playfield_remaining_heights = game.playfield_remaining_heights(self)
        result = bottom_most_blocks.any? do |bottom_most_block|
          playfield_column = @column + bottom_most_block[:column_index]
          playfield_remaining_heights[playfield_column] &&
            @row + bottom_most_block[:row_index] >= playfield_remaining_heights[playfield_column] - 1
        end
        if result && !game.hypothetical?
          @stopped = result
          game.consider_eliminating_lines
          @game.consider_adding_tetromino
        end
        result
      end
      
      # Returns bottom-most blocks of a tetromino, which could be from multiple rows depending on shape (e.g. T)
      def bottom_most_blocks
        width.times.map do |column_index|
          row_blocks_with_row_index = @blocks.each_with_index.to_a.reverse.detect do |row_blocks, row_index|
            !row_blocks[column_index].clear?
          end
          bottom_most_block = row_blocks_with_row_index[0][column_index]
          bottom_most_block_row = row_blocks_with_row_index[1]
          {
            block: bottom_most_block,
            row_index: bottom_most_block_row,
            column_index: column_index
          }
        end
      end
      
      def bottom_most_block_for_column(column)
        bottom_most_blocks.detect {|bottom_most_block| (@column + bottom_most_block[:column_index]) == column}
      end
      
      def right_blocked?
        (@column == game.playfield_width - width) ||
          right_most_blocks.any? { |right_most_block|
            (@row + right_most_block[:row_index]) >= 0 &&
              playfield[@row + right_most_block[:row_index]][@column + right_most_block[:column_index] + 1].occupied?
          }
      end
      
      # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
      def right_most_blocks
        @blocks.each_with_index.map do |row_blocks, row_index|
          column_block_with_column_index = row_blocks.each_with_index.to_a.reverse.detect do |column_block, column_index|
            !column_block.clear?
          end
          if column_block_with_column_index
            right_most_block = column_block_with_column_index[0]
            {
              block: right_most_block,
              row_index: row_index,
              column_index: column_block_with_column_index[1]
            }
          end
        end.compact
      end
      
      def left_blocked?
        (@column == 0) ||
          left_most_blocks.any? { |left_most_block|
            (@row + left_most_block[:row_index]) >= 0 &&
              playfield[@row + left_most_block[:row_index]][@column + left_most_block[:column_index] - 1].occupied?
          }
      end

      # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
      def left_most_blocks
        @blocks.each_with_index.map do |row_blocks, row_index|
          column_block_with_column_index = row_blocks.each_with_index.to_a.detect do |column_block, column_index|
            !column_block.clear?
          end
          if column_block_with_column_index
            left_most_block = column_block_with_column_index[0]
            {
              block: left_most_block,
              row_index: row_index,
              column_index: column_block_with_column_index[1]
            }
          end
        end.compact
      end
            
      def width
        @blocks[0].size
      end
      
      def height
        @blocks.size
      end
      
      def down!(instant: false)
        launch! if preview?
        unless stopped?
          block_count = 1
          if instant
            remaining_height, bottom_touching_block = remaining_height_and_bottom_touching_block
            block_count = remaining_height - @row
          end
          new_row = @row + block_count
          update_playfield(new_row, @column)
        end
      end
      
      def left!
        unless left_blocked?
          new_column = @column - 1
          update_playfield(@row, new_column)
        end
      end
      
      def right!
        unless right_blocked?
          new_column = @column + 1
          update_playfield(@row, new_column)
        end
      end
      
      # Rotate in specified direcation, which can be :right (clockwise) or :left (counterclockwise)
      def rotate!(direction)
        return if stopped?
        can_rotate = nil
        new_blocks = nil
        game.hypothetical do
          hypothetical_rotated_tetromino = hypothetical_tetromino
          new_blocks = hypothetical_rotated_tetromino.rotate_blocks(direction)
          can_rotate = !hypothetical_rotated_tetromino.stopped? && !hypothetical_rotated_tetromino.right_blocked? && !hypothetical_rotated_tetromino.left_blocked?
        end
        if can_rotate
          remove_from_playfield
          self.orientation = ORIENTATIONS[ORIENTATIONS.rotate(direction == :right ? -1 : 1).index(@orientation)]
          self.blocks = new_blocks
          update_playfield(@row, @column)
        end
      rescue => e
        puts e.full_message
      end
      
      def rotate_blocks(direction)
        new_blocks = Matrix[*@blocks].transpose.to_a
        if direction == :right
          new_blocks = new_blocks.map(&:reverse)
        else
          new_blocks = new_blocks.reverse
        end
        Matrix[*new_blocks].to_a
      end
      
      def hypothetical_tetromino
        clone.tap do |hypo_clone|
          remove_from_playfield
          hypo_clone.blocks = @blocks.map do |row_blocks|
            row_blocks.map do |column_block|
              column_block.clone
            end
          end
        end
      end
      
      def remaining_height_and_bottom_touching_block
        playfield_remaining_heights = game.playfield_remaining_heights(self)
        bottom_most_blocks.map do |bottom_most_block|
          playfield_column = @column + bottom_most_block[:column_index]
          [playfield_remaining_heights[playfield_column] - (bottom_most_block[:row_index] + 1), bottom_most_block]
        end.min_by(&:first)
      end
      
      def default_blocks
        case @letter
        when :I
          [
            [block, block, block, block]
          ]
        when :J
          [
            [block, block, block],
            [empty, empty, block],
          ]
        when :L
          [
            [block, block, block],
            [block, empty, empty],
          ]
        when :O
          [
            [block, block],
            [block, block],
          ]
        when :S
          [
            [empty, block, block],
            [block, block, empty],
          ]
        when :T
          [
            [block, block, block],
            [empty, block, empty],
          ]
        when :Z
          [
            [block, block, empty],
            [empty, block, block],
          ]
        end
      end
      
      def color
        LETTER_COLORS[@letter]
      end
      
      def include_block?(block)
        @blocks.flatten.include?(block)
      end
      
      private
      
      def block
        Block.new(color)
      end
      
      def empty
        Block.new
      end
      
      def update_playfield_block(&updater)
        @row.upto(@row + height - 1) do |playfield_row|
          @column.upto(@column + width - 1) do |playfield_column|
            row_index = playfield_row - @row
            column_index = playfield_column - @column
            updater.call(playfield_row, playfield_column, row_index, column_index)
          end
        end
      end
    end
  end
end