Happy New Year: Settlers of Catan in SQL
One of the best New Year presents I've ever got was a copy of the German-style board game, The Settlers of Catan.
This game has brought me and my friends many an hour of good entertainment.

The game is played on a hexagon field with 19 hexagon tiles (3 + 4 + 5 + 4 + 3), which have to be randomly put into appropriate places. In addition, 18 of those tiles have a score token on it, which has to be put there, also randomly, albeit with some limitations. Finally, 9 more pieces (harbors) have to be randomly put to their places, which are printed on the game field.
Today, we'll be implementing the Almanac Variable Catan setup using SQL.
Tiles
We will be using trapezoidal encoding for the tiles, so that each tile is defined by its position on two axes at 60° to each other, starting at the NW corner. The x axis is diagonal (SW to NE), the y axis is horizontal. The hexagons are pointy topped.
We will create a small CTE to define the coordinates of each tile:
WITH tiles (rn, x, y) AS
(
VALUES
(1, 0, 0),
(2, 1, 0),
(3, 2, 0),
(4, 3, 1),
(5, 4, 2),
(6, 4, 3),
(7, 4, 4),
(8, 3, 4),
(9, 2, 4),
(10, 1, 3),
(11, 0, 2),
(12, 0, 1),
(13, 1, 1),
(14, 2, 1),
(15, 3, 2),
(16, 3, 3),
(17, 2, 3),
(18, 1, 2),
(19, 2, 2)
)
SELECT *
FROM tiles
| rn | x | y |
|---|---|---|
| 1 | 0 | 0 |
| 2 | 1 | 0 |
| 3 | 2 | 0 |
| 4 | 3 | 1 |
| 5 | 4 | 2 |
| 6 | 4 | 3 |
| 7 | 4 | 4 |
| 8 | 3 | 4 |
| 9 | 2 | 4 |
| 10 | 1 | 3 |
| 11 | 0 | 2 |
| 12 | 0 | 1 |
| 13 | 1 | 1 |
| 14 | 2 | 1 |
| 15 | 3 | 2 |
| 16 | 3 | 3 |
| 17 | 2 | 3 |
| 18 | 1 | 2 |
| 19 | 2 | 2 |
Resources
We'll be using / for wheat, # for wood, > for rock, " for sheep and . for clay. For the desert we will be using empty space. Those characters are not that descriptive but they make good distinct ASCII art patterns.
We will just put all available resources into a string then shuffle the string using ORDER BY RANDOM():
WITH tiles (rn, x, y) AS
(
VALUES
(1, 0, 0),
(2, 1, 0),
(3, 2, 0),
(4, 3, 1),
(5, 4, 2),
(6, 4, 3),
(7, 4, 4),
(8, 3, 4),
(9, 2, 4),
(10, 1, 3),
(11, 0, 2),
(12, 0, 1),
(13, 1, 1),
(14, 2, 1),
(15, 3, 2),
(16, 3, 3),
(17, 2, 3),
(18, 1, 2),
(19, 2, 2)
),
resources AS
(
SELECT '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
)
SELECT *
FROM tiles
JOIN (
SELECT ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
resource
FROM resources
CROSS JOIN
LATERAL
REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
) tr
USING (rn)
| rn | x | y | resource |
|---|---|---|---|
| 1 | 0 | 0 | ^ |
| 2 | 1 | 0 | # |
| 3 | 2 | 0 | |
| 4 | 3 | 1 | # |
| 5 | 4 | 2 | # |
| 6 | 4 | 3 | " |
| 7 | 4 | 4 | " |
| 8 | 3 | 4 | / |
| 9 | 2 | 4 | . |
| 10 | 1 | 3 | " |
| 11 | 0 | 2 | ^ |
| 12 | 0 | 1 | . |
| 13 | 1 | 1 | # |
| 14 | 2 | 1 | ^ |
| 15 | 3 | 2 | / |
| 16 | 3 | 3 | / |
| 17 | 2 | 3 | " |
| 18 | 1 | 2 | / |
| 19 | 2 | 2 | . |
Scores
Each tile (except the desert) should have a score token on it. The scores are from 2 to 12 (those two dice can yield). Score 7 is not used, there are one token each for the scores 2 to 12 and two tokens each for the rest of the scores. This makes 18 tokens in total.
The tricky part is that the rules say the high-score tiles (6 and 8) should not be adjacent to each other. This means that pure random distribution (the one we were using for the resource tiles) would not work here.
The Almanac says: "You may have to swap tokens to ensure that no red numbers are on adjacent hexes". However, the swapping algorithms, unless carefully elaborated, are notorious to introduce bias, so that some layouts might be slightly under-represented while the others are over-represented.
For this reason, if we see a problem with our score layout, we will not try to fix it, and will just generate another one from scratch, until it's good enough.
To do this, we first will generate an array of scores in random order. Then we will unnest it and join with the tiles and resources tables we generated on the previous steps. One tricky part is that we have 19 tiles but 18 scores, so we have to join them skipping the desert tile, so that each score record gets joined with correct tile record. We will work around this with a simple analytic function which skips a number after the desert tile.
Once we have joined tiles and scores, we need to make sure that no 6's or 8's are on adjacent tiles. For this we will make a self-join of the scores and tiles tables, employing some hexagon grid properties.
How do we know two tiles are adjacent? First of all, the have to be within one unit from each other on either axis, meaning that the difference of either coordinate should be from -1 to 1. Second, they (naturally) should not match, that is at least one coordinate should differ between the tiles. Third (and this is how hexagon grids are different from orthogonal grids), while every square on an orthogonal grid has eight neighbors, a hexagon only has six. It means that even if two coordinates are no more than one unit apart, the tiles can still not be neighbors. This is indeed true for tiles (-1, 1) and (1, -1).
So for every tile, the list of its possible neighbors on the grid is (-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0) and (1, 1). The numbers here are of course offsets from the original tile's coordinates. This list is quite concise, so we'll just employ some tuple predicates to make a join condition. We will reject every layout where exist two tiles which are both either 6 or 8, and the differences between the tiles' coordinates make one of the tuples in the list above.
"Rejecting" a layout means we re-shuffle the original scores array and feed it to another iteration of the recursive CTE. Then we rinse and repeat until we get us a good layout.
Here's the recursive query:
SELECT SETSEED(0.201703);
WITH RECURSIVE
resources AS
(
SELECT '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
),
tiles (rn, x, y) AS
(
VALUES
(1, 0, 0),
(2, 1, 0),
(3, 2, 0),
(4, 3, 1),
(5, 4, 2),
(6, 4, 3),
(7, 4, 4),
(8, 3, 4),
(9, 2, 4),
(10, 1, 3),
(11, 0, 2),
(12, 0, 1),
(13, 1, 1),
(14, 2, 1),
(15, 3, 2),
(16, 3, 3),
(17, 2, 3),
(18, 1, 2),
(19, 2, 2)
),
layout AS
(
SELECT *,
CASE resource
WHEN ' ' THEN
NULL
ELSE
rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
END score_rn
FROM tiles
JOIN (
SELECT ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
resource
FROM resources
CROSS JOIN
LATERAL
REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
) tr
USING (rn)
),
score AS
(
SELECT 1 attempt,
ARRAY_AGG(s ORDER BY RANDOM()) score_array,
NULL::BIGINT desert
FROM generate_series(2, 12) s
CROSS JOIN
generate_series(1, 2) r
WHERE s <> 7
AND NOT (r = 2 AND s IN (2, 12))
UNION ALL
SELECT attempt + 1 attempt,
sa.score_array,
(
SELECT rn
FROM layout
WHERE score_rn IS NULL
) desert
FROM (
SELECT *
FROM score
WHERE EXISTS
(
SELECT NULL
FROM (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s1, score_rn)
JOIN layout
USING (score_rn)
) sc1
JOIN (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s2, score_rn)
JOIN layout
USING (score_rn)
) sc2
ON s1 IN (6, 8)
AND s2 IN (6, 8)
AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
)
) s
CROSS JOIN
LATERAL
(
SELECT ARRAY_AGG(score ORDER BY RANDOM()) score_array
FROM UNNEST(score_array) WITH ORDINALITY q(score, score_rn)
) sa
),
score_good AS
(
SELECT score, score_rn
FROM (
SELECT *
FROM score
ORDER BY
attempt DESC
LIMIT 1
) s
CROSS JOIN
LATERAL
UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
)
SELECT *
FROM score_good
| score | score_rn |
|---|---|
| 8 | 1 |
| 2 | 2 |
| 6 | 3 |
| 10 | 4 |
| 11 | 5 |
| 11 | 6 |
| 9 | 7 |
| 3 | 8 |
| 4 | 9 |
| 8 | 10 |
| 9 | 11 |
| 4 | 12 |
| 12 | 13 |
| 5 | 14 |
| 10 | 15 |
| 5 | 16 |
| 3 | 17 |
| 6 | 18 |
I've added a call to `SETSEED` above so that the results are reproducible.
We can see that it took the query 7 attempts to generate a valid score layout. The first setup had 6 and 8 scores on adjacent tiles 7 and 16; the second one had two 8's in a row on tiles 17 and 18; etc.
Once we have a valid score layout we can join it to the rest of our tables:
SELECT SETSEED(0.201703);
WITH RECURSIVE
resources AS
(
SELECT '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources
),
tiles (rn, x, y) AS
(
VALUES
(1, 0, 0),
(2, 1, 0),
(3, 2, 0),
(4, 3, 1),
(5, 4, 2),
(6, 4, 3),
(7, 4, 4),
(8, 3, 4),
(9, 2, 4),
(10, 1, 3),
(11, 0, 2),
(12, 0, 1),
(13, 1, 1),
(14, 2, 1),
(15, 3, 2),
(16, 3, 3),
(17, 2, 3),
(18, 1, 2),
(19, 2, 2)
),
layout AS
(
SELECT *,
CASE resource
WHEN ' ' THEN
NULL
ELSE
rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
END score_rn
FROM tiles
JOIN (
SELECT ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
resource
FROM resources
CROSS JOIN
LATERAL
REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
) tr
USING (rn)
),
score AS
(
SELECT 1 attempt,
ARRAY_AGG(s ORDER BY RANDOM()) score_array,
NULL::BIGINT desert
FROM generate_series(2, 12) s
CROSS JOIN
generate_series(1, 2) r
WHERE s <> 7
AND NOT (r = 2 AND s IN (2, 12))
UNION ALL
SELECT attempt + 1 attempt,
sa.score_array,
(
SELECT rn
FROM layout
WHERE score_rn IS NULL
) desert
FROM (
SELECT *
FROM score
WHERE EXISTS
(
SELECT NULL
FROM (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s1, score_rn)
JOIN layout
USING (score_rn)
) sc1
JOIN (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s2, score_rn)
JOIN layout
USING (score_rn)
) sc2
ON s1 IN (6, 8)
AND s2 IN (6, 8)
AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
)
) s
CROSS JOIN
LATERAL
(
SELECT ARRAY_AGG(score ORDER BY RANDOM()) score_array
FROM UNNEST(s.score_array) WITH ORDINALITY q(score, score_rn)
) sa
),
score_good AS
(
SELECT score, score_rn, attempt
FROM (
SELECT *
FROM score
ORDER BY
attempt DESC
LIMIT 1
) s
CROSS JOIN
LATERAL
UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
)
SELECT *
FROM layout
LEFT JOIN
score_good
USING (score_rn)
ORDER BY
rn;
| score_rn | rn | x | y | resource | score | attempt |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | # | 11 | 5 |
| 2 | 2 | 1 | 0 | " | 11 | 5 |
| 3 | 3 | 2 | 0 | . | 8 | 5 |
| 4 | 4 | 3 | 1 | # | 10 | 5 |
| 5 | 5 | 4 | 2 | / | 8 | 5 |
| 6 | 6 | 4 | 3 | " | 3 | 5 |
| 7 | 7 | 4 | 4 | ^ | 3 | 5 |
| None | 8 | 3 | 4 | None | None | |
| 8 | 9 | 2 | 4 | / | 4 | 5 |
| 9 | 10 | 1 | 3 | / | 5 | 5 |
| 10 | 11 | 0 | 2 | # | 9 | 5 |
| 11 | 12 | 0 | 1 | # | 6 | 5 |
| 12 | 13 | 1 | 1 | " | 9 | 5 |
| 13 | 14 | 2 | 1 | ^ | 4 | 5 |
| 14 | 15 | 3 | 2 | . | 2 | 5 |
| 15 | 16 | 3 | 3 | ^ | 5 | 5 |
| 16 | 17 | 2 | 3 | " | 10 | 5 |
| 17 | 18 | 1 | 2 | / | 12 | 5 |
| 18 | 19 | 2 | 2 | . | 6 | 5 |
We have all the data for the resource tiles now.
Harbors
Harbors are pretty straightforward: they are completely random and their positions are fixed. We just need to put them into a string, split the string and randomly re-order the resulting table.
Harbors are technically not placed on hexagons, however, they are exactly there where the hexagons' centers would be if there were hexagons outside the main field. We can encode their positions using the grid coordinates as well. We will also add two numbers for the piers orientations (they will help us with the visualization later):
WITH RECURSIVE
resources AS
(
SELECT '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources,
'/#^' || CHR(34) || '.????'::TEXT harbor_resources
),
harbors (rn, x, y, pier1, pier2) AS
(
VALUES
(1, -1, -1, 0, 1),
(2, 1, -1, 1, 2),
(3, 3, 0, 1, 2),
(4, 5, 2, 2, 3),
(5, 5, 4, 3, 4),
(6, 4, 5, 3, 4),
(7, 2, 5, 4, 5),
(8, 0, 3, 5, 0),
(9, -1, 1, 5, 0)
),
harbor_resources AS
(
SELECT '/#>".????'::TEXT harbor_resources
)
SELECT resource, rn, x, y, pier1, pier2
FROM harbors
CROSS JOIN
resources
JOIN LATERAL
(
SELECT resource, ROW_NUMBER() OVER (ORDER BY RANDOM()) rn
FROM REGEXP_SPLIT_TO_TABLE(harbor_resources, '') q (resource)
) q
USING (rn)
ORDER BY
RANDOM()
| resource | rn | x | y | pier1 | pier2 |
|---|---|---|---|---|---|
| # | 3 | 3 | 0 | 1 | 2 |
| " | 5 | 5 | 4 | 3 | 4 |
| ? | 1 | -1 | -1 | 0 | 1 |
| ? | 8 | 0 | 3 | 5 | 0 |
| ^ | 4 | 5 | 2 | 2 | 3 |
| / | 9 | -1 | 1 | 5 | 0 |
| ? | 7 | 2 | 5 | 4 | 5 |
| ? | 6 | 4 | 5 | 3 | 4 |
| . | 2 | 1 | -1 | 1 | 2 |
Putting it all together
Right now we have all information we need to run the game. However, it would help to visualize the layout.
We'll draw the hexagons using different characters for different types of resources, and place the score token inside each hexagon. We will also put a smaller empty hexagon inside each large one, so that the score token would be clearly visible. Finally, we will put the harbors and their piers on the map.
To do that, we will need to generate character maps for every piece of information we are about to put on the screen.
For the hexagons we will first generate a square using a cross-join of two GENERATE_SERIES then filter it so as to make it a hexagon.
The centers of each hexagons are calculated from their x and y coordinates: x is taken as is; y is shifted by half the respective x value (remember that our axes are at 60° to each other).
The filtering condition for the hexagon is this: if the character is within -1/4 to +1/4 of height, starting from the center, we output it without further ado; if it's between -1/2 to -1/4 (or 1/4 to 1/2 on the other side), we only output it if its horizontal coordinate is no more than twice the distance from the top or bottom, respectively. The first part forms a rectangle inside the hexagon; the second one forms two triangles on the top and on the bottom.
The smaller hexagons are generated using the same formula, only their total height and width are smaller.
The score is printed inside each hexagon's center. We can't just output it as is the way we do with the resource characters, because some of the scores are two-digit. To deal with this, we should break each score into separate characters and put each character into its own place on the screen.
We also need to do harbors and piers. The harbor resource characters are just placed at the coordinates on the grid (with the above formulas in mind). The piers are shifted by approximate values of sines and cosines of their respective angles (encoded as 60° factors). We do not calculate the sines and cosines directly, and rather use approximation tables.
Some of the resulting characters will overlap: for instance, the center of each tile will contain the character from the larger hexagon, the smaller hexagon and the resource. To work around this, we will assign the layer number to each character. The larger hexagons are on the layer 1, the smaller ones on the layer 2, and, finally, the score character are on the topmost layer 3. If several characters have the same coordinates, the one with the highest layer number wins. We will be doing all of them in a single query, that's why the harbors and piers will need to have the layers too, even if they don't overlap with anything. We will assign them the layer 4.
Once the character maps are ready, the rest is business as usual: we generate the field by cross joining two GENERATE_SERIES together, left join the result with the character maps and replace the NULL values from the left join with spaces. Then we group the characters by rows and output the rows in order.
Here's what we got:
SELECT SETSEED(0.201704);
WITH RECURSIVE
resources AS
(
SELECT '////####^^^' || CHR(34) || CHR(34) || CHR(34) || CHR(34) || '... '::TEXT tile_resources,
'/#^' || CHR(34) || '.????'::TEXT harbor_resources
),
tiles (rn, x, y) AS
(
VALUES
(1, 0, 0),
(2, 1, 0),
(3, 2, 0),
(4, 3, 1),
(5, 4, 2),
(6, 4, 3),
(7, 4, 4),
(8, 3, 4),
(9, 2, 4),
(10, 1, 3),
(11, 0, 2),
(12, 0, 1),
(13, 1, 1),
(14, 2, 1),
(15, 3, 2),
(16, 3, 3),
(17, 2, 3),
(18, 1, 2),
(19, 2, 2)
),
harbors (rn, x, y, pier1, pier2) AS
(
VALUES
(1, -1, -1, 0, 1),
(2, 1, -1, 1, 2),
(3, 3, 0, 1, 2),
(4, 5, 2, 2, 3),
(5, 5, 4, 3, 4),
(6, 4, 5, 3, 4),
(7, 2, 5, 4, 5),
(8, 0, 3, 5, 0),
(9, -1, 1, 5, 0)
),
score AS
(
SELECT 1 attempt,
ARRAY_AGG(s ORDER BY RANDOM()) score_array
FROM generate_series(2, 12) s
CROSS JOIN
generate_series(1, 2) r
WHERE s <> 7
AND NOT (r = 2 AND s IN (2, 12))
UNION ALL
SELECT attempt + 1 attempt,
sa.score_array
FROM (
SELECT *
FROM score
WHERE EXISTS
(
SELECT NULL
FROM (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s1, rn)
JOIN tiles
USING (rn)
) sc1
JOIN (
SELECT *
FROM UNNEST(score_array) WITH ORDINALITY q(s2, rn)
JOIN tiles t
USING (rn)
) sc2
ON s1 IN (6, 8)
AND s2 IN (6, 8)
AND ((sc1.x - sc2.x), (sc1.y - sc2.y)) IN ((-1, -1), (-1, 0), (0, -1), (0, 1), (1, 0), (1, 1))
)
) s
CROSS JOIN
LATERAL
(
SELECT ARRAY_AGG(score ORDER BY RANDOM()) score_array
FROM UNNEST(score_array) WITH ORDINALITY q(score, score_rn)
) sa
),
score_good AS
(
SELECT score, score_rn
FROM (
SELECT *
FROM score
ORDER BY
attempt DESC
LIMIT 1
) s
CROSS JOIN
LATERAL
UNNEST(score_array) WITH ORDINALITY q (score, score_rn)
),
layout AS
(
SELECT *
FROM (
SELECT *,
CASE resource
WHEN ' ' THEN
NULL
ELSE
rn + SUM(CASE resource WHEN ' ' THEN -1 ELSE 0 END) OVER (ORDER BY rn)
END score_rn
FROM tiles
JOIN (
SELECT ROW_NUMBER() OVER (ORDER BY RANDOM()) rn,
resource
FROM resources
CROSS JOIN
LATERAL
REGEXP_SPLIT_TO_TABLE(tile_resources, '') q (resource)
) tr
USING (rn)
) t
LEFT JOIN
score_good
USING (score_rn)
ORDER BY
rn
)
SELECT row
FROM (
SELECT r,
STRING_AGG(COALESCE(letter, ' '), '' ORDER BY c) AS row
FROM generate_series(0, 70) r
CROSS JOIN
generate_series(0, 89) c
LEFT JOIN
(
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY r, c ORDER BY layer DESC) rn
FROM (
SELECT 10 height,
16 width
) d
CROSS JOIN
LATERAL
(
SELECT letter, r, c, layer
FROM layout
CROSS JOIN
LATERAL
(
SELECT height * x + 15 center_r,
width * y - (width / 2)::INT * x + 24 center_c
) c
CROSS JOIN
LATERAL
(
SELECT *
FROM (
SELECT 1 layer, resource letter, center_r + rs r, center_c + cs c
FROM (
SELECT height * 1.5 * 0.8 th, width * 0.9 tw
) t
CROSS JOIN
generate_series(-(th / 2)::INT, (th / 2)::INT) rs
CROSS JOIN
generate_series(-(tw / 2)::INT, (tw / 2)::INT ) cs
CROSS JOIN
LATERAL
(
SELECT rs::FLOAT / th rsf, cs::FLOAT / tw csf
) f
WHERE rsf BETWEEN -0.25 AND 0.25
OR
ABS(csf) BETWEEN 0 AND 1 - ABS(rsf * 2)
UNION ALL
SELECT 2 layer, ' ', center_r + rs r, center_c + cs c
FROM (
SELECT height * 1.5 * 0.35 th, width * 0.35 tw
) t
CROSS JOIN
generate_series(-(th / 2)::INT, (th / 2)::INT) rs
CROSS JOIN
generate_series(-(tw / 2)::INT, (tw / 2)::INT ) cs
CROSS JOIN
LATERAL
(
SELECT rs::FLOAT / th rsf, cs::FLOAT / tw csf
) f
WHERE rsf BETWEEN -0.25 AND 0.25
OR
ABS(csf) BETWEEN 0 AND 1 - ABS(rsf * 2)
UNION ALL
SELECT 3 layer, score_letter letter, center_r r, center_c + pos - 1 c
FROM REGEXP_SPLIT_TO_TABLE(score::TEXT, '') WITH ORDINALITY l(score_letter, pos)
) q
) q2
UNION ALL
SELECT letter, r, c, 4 layer
FROM harbors
JOIN LATERAL
(
SELECT resource, ROW_NUMBER() OVER (ORDER BY RANDOM()) rn
FROM resources
CROSS JOIN
LATERAL
REGEXP_SPLIT_TO_TABLE(harbor_resources, '') q (resource)
) q2
USING (rn)
CROSS JOIN
LATERAL
(
SELECT height * x + 15 center_r,
width * y - (width / 2)::INT * x + 25 center_c
) c
CROSS JOIN
LATERAL
(
SELECT resource letter, center_r r, center_c c
UNION ALL
SELECT letter, r, c
FROM (
SELECT pier1
UNION ALL
SELECT pier2
) p (pier)
CROSS JOIN
LATERAL
(
SELECT SUBSTRING('|\/|\/', (pier + 1), 1) letter,
center_r + ((ARRAY[0.4, 0.2, -0.2, -0.4, -0.2, 0.2])[pier + 1] * height * 1.5 * 0.8)::INT r,
center_c + ((ARRAY[0, 0.3, 0.3, 0, -0.3, -0.3])[pier + 1] * width * 0.9)::INT c
) pl
) p2
) q3
) l
WHERE rn = 1
) t
USING (r, c)
GROUP BY
r
) q
ORDER BY
r
| row |
|---|
| . ^ |
| \ / |
| / . " |
| | ///// ..... | """"" |
| ///////// ......... """"""""" |
| /////////////// ............... """"""""""""""" |
| ////// ////// ...... ...... """""" """""" |
| //// //// .... .... """" """" |
| //// 5 //// .... 11 .... """" 8 """" " |
| //// //// .... .... """" """" |
| ////// ////// ...... ...... """""" """""" / |
| /////////////// ............... """"""""""""""" |
| " ///////// " ......... ^ """"""""" " |
| """"" ///// """"" ..... ^^^^^ """"" """"" | |
| """"""""" / """"""""" . ^^^^^^^^^ " """"""""" |
| """"""""""""""" """"""""""""""" ^^^^^^^^^^^^^^^ """"""""""""""" |
| / """""" """""" """""" """""" ^^^^^^ ^^^^^^ """""" """""" |
| """" """" """" """" ^^^^ ^^^^ """" """" |
| ? """" 9 """" """" 5 """" ^^^^ 6 ^^^^ """" 4 """" |
| """" """" """" """" ^^^^ ^^^^ """" """" |
| \ """""" """""" """""" """""" ^^^^^^ ^^^^^^ """""" """""" |
| """"""""""""""" """"""""""""""" ^^^^^^^^^^^^^^^ """"""""""""""" |
| ^ """"""""" """"""""" / ^^^^^^^^^ . """"""""" # |
| ^^^^^ """"" """"" ///// ^^^^^ ..... """"" ##### |
| ^^^^^^^^^ " " ///////// ^ ......... " ######### |
| ^^^^^^^^^^^^^^^ /////////////// ............... ############### |
| ^^^^^^ ^^^^^^ ////// ////// ...... ...... ###### ###### \ |
| ^^^^ ^^^^ //// //// .... .... #### #### |
| ^^^^ 11 ^^^^ //// 3 //// .... 9 .... #### 12 #### ? |
| ^^^^ ^^^^ //// //// .... .... #### #### |
| ^^^^^^ ^^^^^^ ////// ////// ...... ...... ###### ###### / |
| ^^^^^^^^^^^^^^^ /////////////// ............... ############### |
| ^^^^^^^^^ # / ///////// # ......... # ######### |
| ^^^^^ ##### ///// ///// ##### ..... ##### ##### |
| ^ ######### ///////// / ######### . ######### # |
| ############### /////////////// ############### ############### |
| / ###### ###### ////// ////// ###### ###### ###### ###### |
| #### #### //// //// #### #### #### #### |
| / #### 6 #### //// 10 //// #### 4 #### #### 2 #### |
| #### #### //// //// #### #### #### #### |
| \ ###### ###### ////// ////// ###### ###### ###### ###### |
| ############### /////////////// ############### ############### |
| ######### / ///////// . ######### ^ ######### |
| ##### ///// ///// ..... ##### ^^^^^ ##### | |
| # ///////// / ......... # ^^^^^^^^^ # |
| /////////////// ............... ^^^^^^^^^^^^^^^ |
| ////// ////// ...... ...... ^^^^^^ ^^^^^^ \ |
| //// //// .... .... ^^^^ ^^^^ |
| //// 10 //// .... 8 .... ^^^^ 3 ^^^^ # |
| //// //// .... .... ^^^^ ^^^^ |
| ////// ////// ...... ...... ^^^^^^ ^^^^^^ |
| /////////////// ............... ^^^^^^^^^^^^^^^ |
| ///////// ......... ^^^^^^^^^ |
| | ///// ..... | ^^^^^ |
| / . ^ |
| / \ |
| ? ? |
You can view the queries here: https://github.com/quassnoi/explain-extended-2018
Let this New Year bring you the joy of playing with your friends and family!
Previous New Year posts:
Subscribe in a reader
saddiction
Phillipwot
9 Oct 19 at 15:11