DEV Community 👩‍💻👨‍💻

Cover image for Quarto Game, Custom Shapes, and Canvas Shape DSL Tutorial
Andy Maleh
Andy Maleh

Posted on • Updated on

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!!!

Quarto Board Game

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

Classic Quarto

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

Quarto Computer Game

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Rest of the Quarto code:

Views:

https://github.com/AndyObtiva/glimmer-dsl-swt/tree/master/samples/elaborate/quarto/view

Models:

https://github.com/AndyObtiva/glimmer-dsl-swt/tree/master/samples/elaborate/quarto/model

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Line

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Rectangle

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Rectangle Round

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Rectangle Round Angles

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Oval

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Arc

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Polyline

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Polygon

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Text

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Image

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Image

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Oval Background Pattern Gradient

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Oval Foreground Pattern Gradient

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Oval

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Polyline Line Join Miter Line Cap Flat

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Polyline Line Join Round Line Cap Round

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
Enter fullscreen mode Exit fullscreen mode

Canvas Shape DSL Polyline Line Join Miter Line Cap Flat

Happy Glimmering!

Top comments (0)

Create an Account!

👀 Just want to lurk?

That's fine, you can still create an account and turn on features like 🌚 dark mode.