mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-21 08:01:30 -07:00
Files

This uses a tile shape very similar to the hat, but the tiling _structure_ is totally changed so that there aren't any reflected copies of the tile. I'm not sure how much difference this makes to gameplay: the two tilings are very similar for Loopy purposes. But the code was fun to write, and I think the Spectre shape is noticeably prettier, so I'm adding this to the collection anyway. The test programs also generate a pile of SVG images used in the companion article on my website.
418 lines
12 KiB
C
418 lines
12 KiB
C
/*
|
|
* Common code between spectre-test and spectre-gen, since both of
|
|
* them want to output SVG graphics.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "puzzles.h"
|
|
#include "tree234.h"
|
|
#include "spectre-internal.h"
|
|
#include "spectre-tables-extra.h"
|
|
#include "spectre-help.h"
|
|
|
|
struct HexData {
|
|
const int *edges;
|
|
};
|
|
|
|
static const struct HexData hexdata[] = {
|
|
#define HEXDATA_ENTRY(x) { edges_##x },
|
|
HEX_LETTERS(HEXDATA_ENTRY)
|
|
#undef HEXDATA_ENTRY
|
|
};
|
|
|
|
const char *hex_names[10] = {
|
|
"G", "D", "J", "L", "X", "P", "S", "F", "Y",
|
|
"" /* NO_HEX */
|
|
};
|
|
|
|
Graphics *gr_new(const char *filename, double xmin, double xmax,
|
|
double ymin, double ymax, double scale)
|
|
{
|
|
Graphics *gr = snew(Graphics);
|
|
if (!strcmp(filename, "-")) {
|
|
gr->fp = stdout;
|
|
gr->close_file = false;
|
|
} else {
|
|
gr->fp = fopen(filename, "w");
|
|
if (!gr->fp) {
|
|
fprintf(stderr, "%s: open: %s\n", filename, strerror(errno));
|
|
exit(1);
|
|
}
|
|
gr->close_file = true;
|
|
}
|
|
|
|
fprintf(gr->fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" "
|
|
"standalone=\"no\"?>\n");
|
|
fprintf(gr->fp, "<svg xmlns=\"http://www.w3.org/2000/svg\" "
|
|
"version=\"1.1\" width=\"%f\" height=\"%f\">\n",
|
|
(xmax - xmin) * scale, (ymax - ymin) * scale);
|
|
|
|
gr->absscale = fabs(scale);
|
|
gr->xoff = -xmin * scale;
|
|
gr->xscale = scale;
|
|
/* invert y axis for SVG top-down coordinate system */
|
|
gr->yoff = ymax * scale;
|
|
gr->yscale = -scale;
|
|
|
|
/* Defaults, which can be overridden by the caller immediately
|
|
* after this constructor returns */
|
|
gr->jigsaw_mode = false;
|
|
gr->vertex_blobs = true;
|
|
gr->number_cells = true;
|
|
gr->four_colour = false;
|
|
gr->arcs = false;
|
|
gr->linewidth = 1.5;
|
|
|
|
gr->started = false;
|
|
|
|
return gr;
|
|
}
|
|
|
|
void gr_free(Graphics *gr)
|
|
{
|
|
if (!gr)
|
|
return;
|
|
fprintf(gr->fp, "</svg>\n");
|
|
if (gr->close_file)
|
|
fclose(gr->fp);
|
|
sfree(gr);
|
|
}
|
|
|
|
static void gr_ensure_started(Graphics *gr)
|
|
{
|
|
if (gr->started)
|
|
return;
|
|
|
|
fprintf(gr->fp, "<style type=\"text/css\">\n");
|
|
fprintf(gr->fp, "path { fill: none; stroke: black; stroke-width: %f; "
|
|
"stroke-linejoin: round; stroke-linecap: round; }\n",
|
|
gr->linewidth);
|
|
fprintf(gr->fp, "text { fill: black; font-family: Sans; "
|
|
"text-anchor: middle; text-align: center; }\n");
|
|
if (gr->four_colour) {
|
|
fprintf(gr->fp, ".c0 { fill: rgb(255, 178, 178); }\n");
|
|
fprintf(gr->fp, ".c1 { fill: rgb(255, 255, 178); }\n");
|
|
fprintf(gr->fp, ".c2 { fill: rgb(178, 255, 178); }\n");
|
|
fprintf(gr->fp, ".c3 { fill: rgb(153, 153, 255); }\n");
|
|
} else {
|
|
fprintf(gr->fp, ".G { fill: rgb(255, 128, 128); }\n");
|
|
fprintf(gr->fp, ".G1 { fill: rgb(255, 64, 64); }\n");
|
|
fprintf(gr->fp, ".F { fill: rgb(255, 192, 128); }\n");
|
|
fprintf(gr->fp, ".Y { fill: rgb(255, 255, 128); }\n");
|
|
fprintf(gr->fp, ".S { fill: rgb(128, 255, 128); }\n");
|
|
fprintf(gr->fp, ".D { fill: rgb(128, 255, 255); }\n");
|
|
fprintf(gr->fp, ".P { fill: rgb(128, 128, 255); }\n");
|
|
fprintf(gr->fp, ".X { fill: rgb(192, 128, 255); }\n");
|
|
fprintf(gr->fp, ".J { fill: rgb(255, 128, 255); }\n");
|
|
fprintf(gr->fp, ".L { fill: rgb(128, 128, 128); }\n");
|
|
fprintf(gr->fp, ".optional { stroke-dasharray: 5; }\n");
|
|
fprintf(gr->fp, ".arrow { fill: rgba(0, 0, 0, 0.2); "
|
|
"stroke: none; }\n");
|
|
}
|
|
fprintf(gr->fp, "</style>\n");
|
|
|
|
gr->started = true;
|
|
}
|
|
|
|
/* Logical coordinates in our mathematical space */
|
|
GrCoords gr_logcoords(Point p)
|
|
{
|
|
double rt3o2 = sqrt(3) / 2;
|
|
GrCoords r = {
|
|
p.coeffs[0] + rt3o2 * p.coeffs[1] + 0.5 * p.coeffs[2],
|
|
p.coeffs[3] + rt3o2 * p.coeffs[2] + 0.5 * p.coeffs[1],
|
|
};
|
|
return r;
|
|
}
|
|
|
|
/* Physical coordinates in the output image */
|
|
GrCoords gr_log2phys(Graphics *gr, GrCoords c)
|
|
{
|
|
c.x = gr->xoff + gr->xscale * c.x;
|
|
c.y = gr->yoff + gr->yscale * c.y;
|
|
return c;
|
|
}
|
|
GrCoords gr_physcoords(Graphics *gr, Point p)
|
|
{
|
|
return gr_log2phys(gr, gr_logcoords(p));
|
|
}
|
|
|
|
void gr_draw_text(Graphics *gr, GrCoords logpos, double logheight,
|
|
const char *text)
|
|
{
|
|
GrCoords pos;
|
|
double height;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
pos = gr_log2phys(gr, logpos);
|
|
height = gr->absscale * logheight;
|
|
fprintf(gr->fp, "<text style=\"font-size: %fpx\" x=\"%f\" y=\"%f\">"
|
|
"%s</text>\n", height, pos.x, pos.y + 0.35 * height, text);
|
|
}
|
|
|
|
void gr_draw_path(Graphics *gr, const char *classes, const GrCoords *phys,
|
|
size_t n, bool closed)
|
|
{
|
|
size_t i;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
fprintf(gr->fp, "<path class=\"%s\" d=\"", classes);
|
|
for (i = 0; i < n; i++) {
|
|
GrCoords c = phys[i];
|
|
if (i == 0)
|
|
fprintf(gr->fp, "M %f %f", c.x, c.y);
|
|
else if (gr->arcs)
|
|
fprintf(gr->fp, "A %f %f 10 0 %zu %f %f",
|
|
gr->absscale, gr->absscale, i&1, c.x, c.y);
|
|
else
|
|
fprintf(gr->fp, "L %f %f", c.x, c.y);
|
|
}
|
|
if (gr->arcs) {
|
|
/* Explicitly return to the starting point so as to curve the
|
|
* final edge */
|
|
fprintf(gr->fp, "A %f %f 10 0 0 %f %f",
|
|
gr->absscale, gr->absscale, phys[0].x, phys[0].y);
|
|
}
|
|
if (closed)
|
|
fprintf(gr->fp, " z");
|
|
fprintf(gr->fp, "\"/>\n");
|
|
}
|
|
|
|
void gr_draw_blob(Graphics *gr, const char *classes, GrCoords log,
|
|
double logradius)
|
|
{
|
|
GrCoords centre;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
centre = gr_log2phys(gr, log);
|
|
fprintf(gr->fp, "<circle class=\"%s\" cx=\"%f\" cy=\"%f\" r=\"%f\"/>\n",
|
|
classes, centre.x, centre.y, gr->absscale * logradius);
|
|
}
|
|
|
|
void gr_draw_hex(Graphics *gr, unsigned index, Hex htype,
|
|
const Point *vertices)
|
|
{
|
|
size_t i;
|
|
Point centre;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
/* Draw the actual hexagon, in its own colour */
|
|
if (!gr->jigsaw_mode) {
|
|
GrCoords phys[6];
|
|
for (i = 0; i < 6; i++)
|
|
phys[i] = gr_physcoords(gr, vertices[i]);
|
|
gr_draw_path(gr, (index == 7 && htype == NO_HEX ?
|
|
"optional" : hex_names[htype]), phys, 6, true);
|
|
} else {
|
|
GrCoords phys[66];
|
|
size_t pos = 0;
|
|
const struct HexData *hd = &hexdata[htype];
|
|
|
|
for (i = 0; i < 6; i++) {
|
|
int edge_type = hd->edges[i];
|
|
int sign = edge_type < 0 ? -1 : +1;
|
|
int edge_abs = abs(edge_type);
|
|
int left_sign = (edge_abs & 4) ? sign : edge_type == 0 ? +1 : 0;
|
|
int mid_sign = (edge_abs & 2) ? sign : 0;
|
|
int right_sign = (edge_abs & 1) ? sign : edge_type == 0 ? -1 : 0;
|
|
|
|
GrCoords start = gr_physcoords(gr, vertices[i]);
|
|
GrCoords end = gr_physcoords(gr, vertices[(i+1) % 6]);
|
|
GrCoords x = { (end.x - start.x) / 7, (end.y - start.y) / 7 };
|
|
GrCoords y = { -x.y, +x.x };
|
|
|
|
#define addpoint(X, Y) do { \
|
|
GrCoords p = { \
|
|
start.x + (X) * x.x + (Y) * y.x, \
|
|
start.y + (X) * x.y + (Y) * y.y, \
|
|
}; \
|
|
phys[pos++] = p; \
|
|
} while (0)
|
|
|
|
if (sign < 0) {
|
|
int tmp = right_sign;
|
|
right_sign = left_sign;
|
|
left_sign = tmp;
|
|
}
|
|
|
|
addpoint(0, 0);
|
|
if (left_sign) {
|
|
addpoint(1, 0);
|
|
addpoint(2, left_sign);
|
|
addpoint(2, 0);
|
|
}
|
|
if (mid_sign) {
|
|
addpoint(3, 0);
|
|
addpoint(3, mid_sign);
|
|
addpoint(4, mid_sign);
|
|
addpoint(4, 0);
|
|
}
|
|
if (right_sign) {
|
|
addpoint(5, 0);
|
|
addpoint(5, right_sign);
|
|
addpoint(6, 0);
|
|
}
|
|
|
|
#undef addpoint
|
|
|
|
}
|
|
gr_draw_path(gr, hex_names[htype], phys, pos, true);
|
|
}
|
|
|
|
/* Find the centre of the hex */
|
|
for (i = 0; i < 4; i++)
|
|
centre.coeffs[i] = 0;
|
|
for (i = 0; i < 6; i++)
|
|
centre = point_add(centre, vertices[i]);
|
|
for (i = 0; i < 4; i++)
|
|
centre.coeffs[i] /= 6;
|
|
|
|
/* Draw an arrow towards vertex 0 of the hex */
|
|
if (gr->hex_arrows) {
|
|
double ext = 0.6;
|
|
double headlen = 0.3, thick = 0.08, headwid = 0.25;
|
|
GrCoords top = gr_physcoords(gr, vertices[0]);
|
|
GrCoords bot = gr_physcoords(gr, vertices[3]);
|
|
GrCoords mid = gr_physcoords(gr, centre);
|
|
GrCoords base = { mid.x + ext * (bot.x - mid.x),
|
|
mid.y + ext * (bot.y - mid.y) };
|
|
GrCoords tip = { mid.x + ext * (top.x - mid.x),
|
|
mid.y + ext * (top.y - mid.y) };
|
|
GrCoords len = { tip.x - base.x, tip.y - base.y };
|
|
GrCoords perp = { -len.y, +len.x };
|
|
GrCoords basep = { base.x+perp.x*thick, base.y+perp.y*thick };
|
|
GrCoords basen = { base.x-perp.x*thick, base.y-perp.y*thick };
|
|
GrCoords hbase = { tip.x-len.x*headlen, tip.y-len.y*headlen };
|
|
GrCoords headp = { hbase.x+perp.x*thick, hbase.y+perp.y*thick };
|
|
GrCoords headn = { hbase.x-perp.x*thick, hbase.y-perp.y*thick };
|
|
GrCoords headP = { hbase.x+perp.x*headwid, hbase.y+perp.y*headwid };
|
|
GrCoords headN = { hbase.x-perp.x*headwid, hbase.y-perp.y*headwid };
|
|
|
|
GrCoords phys[] = {
|
|
basep, headp, headP, tip, headN, headn, basen
|
|
};
|
|
|
|
gr_draw_path(gr, "arrow", phys, lenof(phys), true);
|
|
}
|
|
|
|
/*
|
|
* Label the hex with its index and type.
|
|
*/
|
|
if (gr->number_cells) {
|
|
char buf[64];
|
|
if (index == (unsigned)-1) {
|
|
if (htype == NO_HEX)
|
|
buf[0] = '\0';
|
|
else
|
|
strcpy(buf, hex_names[htype]);
|
|
} else {
|
|
if (htype == NO_HEX)
|
|
sprintf(buf, "%u", index);
|
|
else
|
|
sprintf(buf, "%u (%s)", index, hex_names[htype]);
|
|
}
|
|
if (buf[0])
|
|
gr_draw_text(gr, gr_logcoords(centre), 1.2, buf);
|
|
}
|
|
}
|
|
|
|
void gr_draw_spectre(Graphics *gr, Hex container, unsigned index,
|
|
const Point *vertices)
|
|
{
|
|
size_t i;
|
|
GrCoords log[14];
|
|
GrCoords centre;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
for (i = 0; i < 14; i++)
|
|
log[i] = gr_logcoords(vertices[i]);
|
|
|
|
/* Draw the actual Spectre */
|
|
{
|
|
GrCoords phys[14];
|
|
char class[16];
|
|
for (i = 0; i < 14; i++)
|
|
phys[i] = gr_log2phys(gr, log[i]);
|
|
if (gr->four_colour) {
|
|
sprintf(class, "c%u", index);
|
|
} else if (index == 1 && container == NO_HEX) {
|
|
sprintf(class, "optional");
|
|
} else {
|
|
sprintf(class, "%s%.0u", hex_names[container], index);
|
|
}
|
|
gr_draw_path(gr, class, phys, 14, true);
|
|
}
|
|
|
|
/* Pick a point to use as the centre of the Spectre for labelling */
|
|
centre.x = (log[5].x + log[6].x + log[11].x + log[12].x) / 4;
|
|
centre.y = (log[5].y + log[6].y + log[11].y + log[12].y) / 4;
|
|
|
|
/*
|
|
* Label the hex with its index and type.
|
|
*/
|
|
if (gr->number_cells && index != (unsigned)-1) {
|
|
char buf[64];
|
|
sprintf(buf, "%u", index);
|
|
gr_draw_text(gr, centre, 1.2, buf);
|
|
}
|
|
}
|
|
|
|
void gr_draw_spectre_from_coords(Graphics *gr, SpectreCoords *sc,
|
|
const Point *vertices)
|
|
{
|
|
Hex h;
|
|
unsigned index;
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
if (gr->four_colour) {
|
|
h = NO_HEX;
|
|
if (sc->index == 1)
|
|
index = 3; /* special colour for odd G1 Spectres */
|
|
else
|
|
index = sc->hex_colour;
|
|
} else if (sc) {
|
|
h = sc->c[0].type;
|
|
index = sc->index;
|
|
} else {
|
|
h = NO_HEX;
|
|
index = -1;
|
|
}
|
|
gr_draw_spectre(gr, h, index, vertices);
|
|
}
|
|
|
|
void gr_draw_extra_edge(Graphics *gr, Point a, Point b)
|
|
{
|
|
GrCoords phys[2];
|
|
|
|
if (!gr)
|
|
return;
|
|
gr_ensure_started(gr);
|
|
|
|
phys[0] = gr_physcoords(gr, a);
|
|
phys[1] = gr_physcoords(gr, b);
|
|
gr_draw_path(gr, "extraedge", phys, 2, false);
|
|
}
|