Quarto Game, Custom Shapes, and Canvas Shape DSL Tutorial

At a Christmas party I attended a couple of weeks ago, I discovered a classic board game called Quarto. In fact, the host of the party who's worked for a major gaming company in the past asked me if I knew how to build it as a computer application. I discounted myself as a non-game-developer who only builds business applications, but then followed that by saying that if it is only a 2D game, it was simple to build. So, the challenge was on!!!

Here is the Quarto classic black board version that I played with during Christmas:

I built Quarto as a computer game in 4-5 days using Glimmer DSL for SWT!

Here is a video demo of Glimmer Quarto:

The key features in Glimmer DSL for SWT that helped me complete it very quickly are Custom Shape support (e.g. building cylinder and cube Custom Shapes and reusing to model Quarto piece Custom Shape) and the effortless Canvas Drag and Drop (e.g. designating one Custom Shape as a drag source and another as a drop target with on_drop listener). I also used affine Transforms to tilt the board by 45 degrees from a standard grid.

The top-level Quarto code is included below, followed by the code of the Quarto piece, cylinder, and cube Custom Shapes, followed by a link to the rest of the code, and then finally a quick tutorial for the Glimmer DSL for SWT Canvas Shape DSL.

Quarto

# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#quarto

# Top-level Quarto board GUI code that visually maps to real GUI
shell(:shell_trim, (:double_buffered unless OS.mac?)) {
  text 'Glimmer Quarto'
  minimum_size BOARD_DIAMETER + AREA_MARGIN + PIECES_AREA_WIDTH + SHELL_MARGIN*2 + (OS.linux? ? 52 : (OS.windows? ? 16 : 0)), BOARD_DIAMETER + 24 + SHELL_MARGIN*2 + (OS.linux? ? 96 : (OS.windows? ? 32 : 0))
  maximum_size BOARD_DIAMETER + AREA_MARGIN + PIECES_AREA_WIDTH + SHELL_MARGIN*2 + (OS.linux? ? 52 : (OS.windows? ? 16 : 0)), BOARD_DIAMETER + 24 + SHELL_MARGIN*2 + (OS.linux? ? 96 : (OS.windows? ? 32 : 0))
  background COLOR_WOOD

  quarto_menu_bar

  @board = board(game: @game, location_x: SHELL_MARGIN, location_y: SHELL_MARGIN)

  @available_pieces_area = available_pieces_area(game: @game, location_x: SHELL_MARGIN + BOARD_DIAMETER + AREA_MARGIN, location_y: SHELL_MARGIN)
  @selected_piece_area = selected_piece_area(game: @game, location_x: SHELL_MARGIN + BOARD_DIAMETER + AREA_MARGIN, location_y: SHELL_MARGIN + AVAILABLE_PIECES_AREA_HEIGHT + AREA_MARGIN)
}

Piece

# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#quarto

require_relative 'cylinder'
require_relative 'cube'

class Quarto
  module View
    class Piece
      include Glimmer::UI::CustomShape

      SIZE_SHORT = 28
      SIZE_TALL = 48
      BASIC_SHAPE_WIDTH = 48
      BASIC_SHAPE_HEIGHT = 28
      LINE_THICKNESS = 2

      options :game, :model, :location_x, :location_y

      before_body do
        @background_color = model.light? ? COLOR_LIGHT_WOOD : COLOR_DARK_WOOD
        @size = model.short? ? SIZE_SHORT : SIZE_TALL
        @shape_location_x = 0
        @shape_location_y = model.short? ? 20 : 0
      end

      body {
        shape(location_x, location_y) {
          if model.is_a?(Model::Piece::Cylinder)
            cylinder(location_x: @shape_location_x, location_y: @shape_location_y, cylinder_height: @size, oval_width: BASIC_SHAPE_WIDTH, oval_height: BASIC_SHAPE_HEIGHT, pitted: model.pitted?, background_color: @background_color, line_thickness: LINE_THICKNESS)
          else
            cube(location_x: @shape_location_x, location_y: @shape_location_y, cube_height: @size, rectangle_width: BASIC_SHAPE_WIDTH, rectangle_height: BASIC_SHAPE_HEIGHT, pitted: model.pitted?, background_color: @background_color, line_thickness: LINE_THICKNESS)
          end
        }
      }
    end
  end
end

Cylinder

# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#quarto

class Quarto
  module View
    class Cylinder
      include Glimmer::UI::CustomShape

      DEFAULT_SIZE = 28

      options :location_x, :location_y, :oval_width, :oval_height, :cylinder_height, :pitted, :background_color, :line_thickness
      alias pitted? pitted

      before_body do
        self.location_x ||= 0
        self.location_y ||= 0
        self.oval_width ||= oval_height || cylinder_height || DEFAULT_SIZE
        self.oval_height ||= oval_width || cylinder_height || DEFAULT_SIZE
        self.cylinder_height ||= oval_width || oval_height || DEFAULT_SIZE
        self.line_thickness ||= 1
      end

      body {
        shape(location_x, location_y) {
          oval(0, cylinder_height, oval_width, oval_height) {
            background background_color

            oval { # draws with foreground :black and has max size within parent by default
              line_width line_thickness
            }
          }
          rectangle(0, oval_height / 2.0, oval_width, cylinder_height) {
            background background_color
          }
          polyline(0, oval_height / 2.0 + cylinder_height, 0, oval_height / 2.0, oval_width, oval_height / 2.0, oval_width, oval_height / 2.0 + cylinder_height) {
            line_width line_thickness
          }
          oval(0, 0, oval_width, oval_height) {
            background background_color

            oval { # draws with foreground :black and has max size within parent by default
              line_width line_thickness
            }
          }
          if pitted?
            oval(oval_width / 4.0, oval_height / 4.0, oval_width / 2.0, oval_height / 2.0) {
              background :black
            }
          end
        }
      }
    end
  end
end

Cube

# From: https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_SAMPLES.md#quarto

class Quarto
  module View
    class Cube
      include Glimmer::UI::CustomShape

      DEFAULT_SIZE = 28

      options :location_x, :location_y, :rectangle_width, :rectangle_height, :cube_height, :pitted, :background_color, :line_thickness
      alias pitted? pitted

      before_body do
        self.location_x ||= 0
        self.location_y ||= 0
        self.rectangle_width ||= rectangle_height || cube_height || DEFAULT_SIZE
        self.rectangle_height ||= rectangle_width || cube_height || DEFAULT_SIZE
        self.cube_height ||= rectangle_width || rectangle_height || DEFAULT_SIZE
        self.line_thickness ||= 1
      end

      body {
        shape(location_x, location_y) {
          polygon(0, cube_height + rectangle_height / 2.0, rectangle_width / 2.0, cube_height, rectangle_width, cube_height + rectangle_height / 2.0, rectangle_width / 2.0, cube_height + rectangle_height) {
            background background_color
          }
          polygon(0, cube_height + rectangle_height / 2.0, rectangle_width / 2.0, cube_height, rectangle_width, cube_height + rectangle_height / 2.0, rectangle_width / 2.0, cube_height + rectangle_height) {
            line_width line_thickness
          }
          rectangle(0, rectangle_height / 2.0, rectangle_width, cube_height) {
            background background_color
          }
          polyline(0, rectangle_height / 2.0 + cube_height, 0, rectangle_height / 2.0, rectangle_width, rectangle_height / 2.0, rectangle_width, rectangle_height / 2.0 + cube_height) {
            line_width line_thickness
          }
          polygon(0, rectangle_height / 2.0, rectangle_width / 2.0, 0, rectangle_width, rectangle_height / 2.0, rectangle_width / 2.0, rectangle_height) {
            background background_color
          }
          polygon(0, rectangle_height / 2.0, rectangle_width / 2.0, 0, rectangle_width, rectangle_height / 2.0, rectangle_width / 2.0, rectangle_height) {
            line_width line_thickness
          }
          line(rectangle_width / 2.0, cube_height + rectangle_height, rectangle_width / 2.0, rectangle_height) {
            line_width line_thickness
          }
          if pitted?
            oval(rectangle_width / 4.0, rectangle_height / 4.0, rectangle_width / 2.0, rectangle_height / 2.0) {
              background :black
            }
          end
        }
      }
    end
  end
end

Rest of the Quarto code:

Views:

Models:

Now, let us get into the Canvas Shape DSL tutorial.

Canvas Shape DSL Tutorial

Below are examples of using the Canvas Shape DSL in Glimmer DSL for SWT.

Example of line (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    line(30, 30, 170, 170) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of rectangle (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    rectangle(30, 50, 140, 100) {
      background :yellow
    }

    rectangle(30, 50, 140, 100) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of rectangle with round corners having 60 degree angles by default (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    rectangle(30, 50, 140, 100, round: true) {
      background :yellow
    }

    rectangle(30, 50, 140, 100, round: true) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of rectangle with round corners having different horizontal and vertical angles (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    rectangle(30, 50, 140, 100, 40, 80) {
      background :yellow
    }

    rectangle(30, 50, 140, 100, 40, 80) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of oval (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    oval(30, 50, 140, 100) {
      background :yellow
    }

    oval(30, 50, 140, 100) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of arc (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    arc(30, 30, 140, 140, 0, 270) {
      background :yellow
    }

    arc(30, 30, 140, 140, 0, 270) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of polyline (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    polyline(30, 50, 50, 170, 70, 120, 90, 150, 110, 30, 130, 100, 150, 50, 170, 135) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of polygon (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    polygon(30, 90, 80, 20, 130, 40, 170, 90, 130, 140, 80, 170, 40, 160) {
      background :yellow
    }

    polygon(30, 90, 80, 20, 130, 40, 170, 90, 130, 140, 80, 170, 40, 160) {
      foreground :red
      line_width 3
    }
  }
}.open

Example of text (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    text(" This is \n rendered text ", 30, 50) {
      background :yellow
      foreground :red
      font height: 25, style: :italic

      rectangle { # automatically scales to match text extent
        foreground :red
        line_width 3
      }
    }
  }
}.open

Example of image (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 512, 542

  canvas {
    background :white

    image(File.expand_path('icons/scaffold_app.png', __dir__), 0, 5)
  }
}.open

Example of image pre-built with a smaller height (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

@image_object = image(File.expand_path('icons/scaffold_app.png', __dir__), height: 200)

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 230

  canvas {
    background :white

    image(@image_object, 0, 5)
  }
}.open

Example of setting background_pattern attribute to a horizontal gradient (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    oval(30, 30, 140, 140) {
      background_pattern 0, 0, 200, 0, rgb(255, 255, 0), rgb(255, 0, 0)
    }
  }
}.open

Example of setting foreground_pattern attribute to a vertical gradient (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    oval(30, 30, 140, 140) {
      foreground_pattern 0, 0, 0, 200, :blue, :green
      line_width 10
    }
  }
}.open

Example of setting line_style attribute to :dashdot (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    oval(30, 50, 140, 100) {
      background :yellow
    }

    oval(30, 50, 140, 100) {
      foreground :red
      line_width 3
      line_style :dashdot
    }
  }
}.open

Example of setting line_width attribute to 10, line_join attribute to :miter (default) and line_cap attribute to :flat (default) (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    polyline(30, 50, 50, 170, 70, 120, 90, 150, 110, 30, 130, 100, 150, 50, 170, 135) {
      foreground :red
      line_width 10
      line_join :miter
      line_cap :flat
    }
  }
}.open

Example of setting line_width attribute to 10, line_join attribute to :round and line_cap attribute to :round (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    polyline(30, 50, 50, 170, 70, 120, 90, 150, 110, 30, 130, 100, 150, 50, 170, 135) {
      foreground :red
      line_width 10
      line_join :round
      line_cap :round
    }
  }
}.open

Example of setting line_width attribute to 10, line_join attribute to :bevel and line_cap attribute to :square (you may copy/paste in girb):

require 'glimmer-dsl-swt'

include Glimmer

shell {
  text 'Canvas Shape DSL'
  minimum_size 200, 220

  canvas {
    background :white

    polyline(30, 50, 50, 170, 70, 120, 90, 150, 110, 30, 130, 100, 150, 50, 170, 135) {
      foreground :red
      line_width 10
      line_join :bevel
      line_cap :square
    }
  }
}.open

Happy Glimmering!

37