# Planar Geometric Pattern Matching

**URL:** <https://ziggit.dev/t/planar-geometric-pattern-matching/17421>\
**Category:** Help\
**Tags:** game\
**Created:** [August 30, 2026, 6:15am UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421 "2026-08-30T06:15:37Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![TheDarkUndoing](https://ziggit.dev/user_avatar/ziggit.dev/thedarkundoing/32/9617_2.png) [@TheDarkUndoing](https://ziggit.dev/u/TheDarkUndoing)\
**Post date:** [August 30, 2026, 6:15am UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/1 "2026-08-30T06:15:37Z")

</div>

![image](https://ziggit.dev/uploads/default/original/2X/b/ba03aa98c35976bc23b28c0511ebc0d495f3c431.png)

_Fig. 1 - Visual representation of a “complete pattern” as seen from the perspective of viewing the board_

_Fig. 2 - A valid game pattern orientation outside the “complete pattern”_\*

_Fig. 3 - Representation of how the board is stored in memory as a 2d Arraylist_\*

For the game I’m making, I need to match certain patterns to identify where a player can convert placed resources into a building. I originally did this by taking a base pattern and rotating 90 degrees to make a “complete pattern”. However, I identified this is not complete because it does not make it possible to match reflections of that pattern. if the base pattern is: Brick(2,2) Glass(3,2) Wheat(3,3) It is impossible to match the pattern in figure 2 without reflecting and translating the center piece back to the same point. This got me thinking maybe I’m doing this wrong.

Should I implement the reflection piece and add to the “complete pattern” or is there a better way to do pattern matching that is suitable for this situation?

_Note: patterns are stored similarly in 1 dimensional slices of resources or a two dimensional slice for the “complete pattern” used by placing on center point and then checking if any pattern is fulfilled._\*

> **Edit adding link to code**
>
> [code](https://git.h4ck.men/h4ckmen/bittyburg)

---

<div class="post-metadata">

**Author:** ![giluis](https://ziggit.dev/user_avatar/ziggit.dev/giluis/32/5886_2.png) [@giluis](https://ziggit.dev/u/giluis)\
**Post date:** [August 30, 2026, 9:26am UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/2 "2026-08-30T09:26:35Z")

</div>

Hey! Sounds like an interesting problem!  
Can you share some code?

---

<div class="post-metadata">

**Author:** ![wabal](https://ziggit.dev/letter_avatar_proxy/v4/letter/w/47e85d/32.png) [@wabal](https://ziggit.dev/u/wabal)\
**Post date:** [August 30, 2026, 10:29am UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/3 "2026-08-30T10:29:14Z")

</div>

Essentially you seem to have a pattern that can be placed on a 2D grid, and any reflection or rotation by 90 degrees of it is also a valid placement. For arbitrary 2D shapes, this makes for 8 different rotation/reflection options, given that you fix one of the squares and rotate around it. I.e. for the three square piece in shape of an L you have:  
oox  
oxx  
ooo  
But also the shape:  
ooo  
oxo  
xxo

is valid.

On the grid, each square has resources, which much match the resources of the pattern.

I think the ‘trick’ you have where you check against a fixed 2D pattern works in some cases but fails in other cases for more complicated shapes.  
For example, suppose you have a shape like:  
xxx  
oxo  
ooo

Where the top left and top right corner require different resources. There is no way to make a fixed pattern in such a case if you also allow for reflections.

I would guess it is probably easiest if you either construct the 8 different patterns once up front and iterate through them, either explicitly or implicitly. Alternatively, if you only have to check one specific pattern at a time, you could make an algorithm that takes the specific reflection and/or rotations that are applied to the shape as input, and then iterate over the squares of the shape to check if the resources match. This last solution seems the easiest to me.

---

<div class="post-metadata">

**Author:** ![castholm](https://ziggit.dev/user_avatar/ziggit.dev/castholm/32/1330_2.png) [@castholm](https://ziggit.dev/u/castholm)\
**Post date:** [August 30, 2026, 12:44pm UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/4 "2026-08-30T12:44:39Z")

</div>

Which approach is best is completely dependent on the answers to questions such as “how large/complex do I expect patterns to get?” and “how many patterns will there be?” and “how easy should it be for developers/modders to define patterns?”.

Your example pattern has certain useful symmetries and could therefore be quite trivially matched against using an algorithm like

```zig
if (tile(x, y) == .glass) {
    if (tile(x + 1, y) == .brick or tile(x - 1, y) == .brick) {
        return tile(x, y + 1) == .wheat or tile(x, y - 1) == .wheat;
    }
    if (tile(x, y + 1) == .brick or tile(x, y - 1) == .brick) {
        return tile(x + 1, y) == .wheat or tile(x - 1, y) == .wheat;
    }
}
return false;

```

but defining patterns in this manner will obviously be a pain in the ass to maintain if you have more than a handful of simple patterns.

I’d probably define patterns like

```zig
const pattern: []const Entry = &.{
    .{ .x = 0, .y = 0, .resource = .brick },
    .{ .x = 1, .y = 0, .resource = .glass },
    .{ .x = 1, .y = 1, .resource = .wheat },
};

```

and use [transformation matrices](https://en.wikipedia.org/wiki/Transformation_matrix) to easily check [all eight rotations/reflections](https://en.wikipedia.org/wiki/Dihedral_group#Matrix_representation):

```zig
const matrices: []const [4]i32 = &.{
    .{ 1, 0, 0, 1 }, // rotate 0 deg
    .{ 1, 0, 0, -1 }, // rotate 0 deg, reflect
    .{ 0, -1, 1, 0 }, // rotate 90 deg
    .{ 0, 1, 1, 0 }, // rotate 90 deg, reflect
    .{ -1, 0, 0, -1 }, // rotate 180 deg
    .{ -1, 0, 0, 1 }, // rotate 180 deg, reflect
    .{ 0, 1, -1, 0 }, // rotate 270 deg
    .{ 0, -1, -1, 0 }, // rotate 270 deg, reflect
};

```

```zig
fn test(grid: Grid, grid_x: i32, grid_y: i32, pattern: []const Entry) bool {
    next_matrix: for (matrices) |m| {
        for (pattern) |p| {
            const x = grid_x + p.x * m[0] + p.y * m[1];
            const y = grid_y + p.x * m[2] + p.y * m[3];
            if (grid.getResource(x, y) != p.resource) {
                continue :next_matrix;
            }
        }
        return true;
    }
    return false;
}

```

This approach is simple to reason about and maintain as you define more complicated patterns. It can also be easily adapted to more complicated rules like “the neighboring tile must belong to category `.mineral` and have a quality value \> 5”. The algorithm will perform some redundant tests for entries where X = 0 or Y = 0, but I doubt this will matter in practice.

---

<div class="post-metadata">

**Author:** ![TheDarkUndoing](https://ziggit.dev/user_avatar/ziggit.dev/thedarkundoing/32/9617_2.png) [@TheDarkUndoing](https://ziggit.dev/u/TheDarkUndoing)\
**Post date:** [August 30, 2026, 3:41pm UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/5 "2026-08-30T15:41:21Z")

</div>

I edited post to link to code

---

<div class="post-metadata">

**Author:** ![TheDarkUndoing](https://ziggit.dev/user_avatar/ziggit.dev/thedarkundoing/32/9617_2.png) [@TheDarkUndoing](https://ziggit.dev/u/TheDarkUndoing)\
**Post date:** [August 30, 2026, 4:15pm UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/6 "2026-08-30T16:15:46Z")

</div>

Thank you for the links. I watched a few vids on matrix transformations. I did not take linear algebra so its this kind of information that is really helpful! I’ll implement the matrix method and test.

I am representing patterns like:

```zig
pub const PatternPoint = struct {
    x: i8,
    y: i8,
    material: game.MaterialType,
    pub fn new(x: i8, y: i8, m: game.MaterialType) PatternPoint {
        return PatternPoint{ .x = x, .y = y, .material = m };
    }
};

```

rotations are done:

```zig
 pub fn pattern_rotations(allocator: std.mem.Allocator, pattern: []card.PatternPoint) ![4][]card.PatternPoint {
        var full_rotations = [_][]card.PatternPoint{&.{}} ** 4;
        const rotation_point = board.Point.new(pattern[0].x, pattern[0].y);
        var position = pattern;

        //Reflections

        //Rotations
        for (0..4) |i| {
            if (i == 0) {
                full_rotations[i] = pattern;
                continue;
            }
            position = try rotatePattern(allocator, position, rotation_point, 90);
            full_rotations[i] = position;
        }
        // log.debug("{any}", .{full_rotations});
        return full_rotations;
    }
pub fn rotatePattern(allocator: std.mem.Allocator, pattern: []const card.PatternPoint, center: board.Point, degrees: f16) ![]card.PatternPoint {
        // var rotated = [_]card.PatternPoint{.{ .material = .Brick, .x = -50, .y = -50 }} ** 10;

        var rotated = try std.ArrayList(card.PatternPoint).initCapacity(allocator, pattern.len);
        defer rotated.deinit(allocator);
        // for (0..pattern.len) |_| {
        // try pattern_arraylist.append(allocator, .{ .material = .Brick, .x = 0, .y = 0 });
        // }
        // const copied_pattern = try pattern_arraylist.toOwnedSlice(allocator);

        const radians = std.math.degreesToRadians(degrees);
        for (pattern, 0..pattern.len) |point, _| {
            // const material_symbol = board.board.board.PieceType.str(board.board.board.PieceType{ .material = point.material });
            // if (point.x == center.x and point.y == center.y) {
            // print("Skipping center block of pattern\n", .{});
            // continue;
            // }
            const point_minus_center_x = (@as(f16, point.x) - center.x);
            const point_minus_center_y = (@as(f16, point.y) - center.y);
            // print("Center: {},{}\n", .{ point_minus_center_x, point_minus_center_y });

            // log.debug("Point: {d},{d}", .{ point.x, point.y });
            // x' = cₓ + cosθ(x−cₓ) − sinθ(y−c_y)
            const rotatedX: f16 = center.x + @cos(radians) * point_minus_center_x - @sin(radians) * point_minus_center_y;

            // y' = c_y + sinθ(x−cₓ) + cosθ(y−c_y)
            const rotatedY: f16 = center.y + @sin(radians) * point_minus_center_x - @cos(radians) * point_minus_center_y;

            // log.debug("Rotated Point: {d},{d}", .{ @round(rotatedX), @round(rotatedY) });
            // log.debug("Rotated Point(float): {d},{d} {s}", .{ rotatedX, rotatedY, material_symbol });

            const rotatedPoint: card.PatternPoint = .{ .material = point.material, .x = @round(rotatedX), .y = @round(rotatedY) };
            try rotated.append(allocator, rotatedPoint);
        }

        // return try allocator.dupe(card.PatternPoint, rotated[0..pattern.len]);
        return rotated.toOwnedSlice(allocator);
        // return rotated;
    }

```

---

<div class="post-metadata">

**Author:** ![castholm](https://ziggit.dev/user_avatar/ziggit.dev/castholm/32/1330_2.png) [@castholm](https://ziggit.dev/u/castholm)\
**Post date:** [August 30, 2026, 4:41pm UTC](https://ziggit.dev/t/planar-geometric-pattern-matching/17421/7 "2026-08-30T16:41:52Z")

</div>

> [@TheDarkUndoing](#):
>
> Thank you for the links. I watched a few vids on matrix transformations. I did not take linear algebra so its this kind of information that is really helpful!

I didn’t either 🙂 I thought linear algebra and matrices were overly complex esoteric math nonsense until I started teaching myself 3D programming and had it explained to me that it’s just a clever way to multiply and add numbers, and that 0s and 1s can be used to turn multiplications/additions into no-ops. [This webgl2fundamentals.org article was quite enlightening for me.](https://webgl2fundamentals.org/webgl/lessons/webgl-2d-matrices.html)

In this case because we’re only working with rotations and reflections and the matrices all follow a set pattern you don’t even really need to think of it as any kind of advanced math, it’s just different permutations of the following:

```zig
xx = x * m[0] + y * m[1]; yy = x * m[2] + y * m[3];

// the 1, 0, 0, 1 pattern cancels out the middle two multiplications,
// (preserving x and y)
xx = x * m[0] + y * m[1]; yy = x * m[2] + y * m[3];
xx = x * 1 + y * 0; yy = x * 0 + y * 1;
xx = x + 0; yy = 0 + y;
xx = x; yy = y;

// the 0, 1, 1, 0 pattern cancels out the outermost two multiplications,
// (swapping x and y)
xx = x * m[0] + y * m[1]; yy = x * m[2] + y * m[3];
xx = x * 0 + y * 1; yy = x * 1 + y * 0;
xx = 0 + y; yy = x + 0;
xx = y; yy = x;

```
