|
@@ -2,11 +2,13 @@ module cosmic.blocks;
|
2
|
2
|
|
3
|
3
|
import cosmic.base;
|
4
|
4
|
import cosmic.drawing;
|
5
|
|
-
|
6
|
5
|
import std.math, std.format;
|
|
6
|
+import std.experimental.logger;
|
7
|
7
|
|
8
|
8
|
final class Board
|
9
|
9
|
{
|
|
10
|
+ immutable width = WIDTH;
|
|
11
|
+ immutable height = HEIGHT;
|
10
|
12
|
Block[HEIGHT][WIDTH] grid;
|
11
|
13
|
Pos startA, startB;
|
12
|
14
|
|
|
@@ -16,7 +18,7 @@ final class Board
|
16
|
18
|
{
|
17
|
19
|
foreach (y; 0..HEIGHT)
|
18
|
20
|
{
|
19
|
|
- grid[x][y] = Block(null, Access.none, Pos(x, y));
|
|
21
|
+ grid[x][y] = Block(null, Pos(x, y));
|
20
|
22
|
}
|
21
|
23
|
}
|
22
|
24
|
|
|
@@ -31,25 +33,56 @@ final class Board
|
31
|
33
|
|
32
|
34
|
string colorA = "red", colorB = "yellow", colorBoth = "orange", colorBg = "#E3EAEA";
|
33
|
35
|
|
|
36
|
+ void tick()
|
|
37
|
+ {
|
|
38
|
+ recalculateAccessibility;
|
|
39
|
+ foreach (x; 0..WIDTH)
|
|
40
|
+ {
|
|
41
|
+ foreach (y; 0..HEIGHT)
|
|
42
|
+ {
|
|
43
|
+ grid[x][y].tick;
|
|
44
|
+ }
|
|
45
|
+ }
|
|
46
|
+ /*
|
|
47
|
+ foreach (ref block; blocks)
|
|
48
|
+ {
|
|
49
|
+ block.tick;
|
|
50
|
+ }
|
|
51
|
+ */
|
|
52
|
+ }
|
|
53
|
+
|
|
54
|
+ void decayCollisions()
|
|
55
|
+ {
|
|
56
|
+ foreach (x; 0..width)
|
|
57
|
+ {
|
|
58
|
+ foreach (y; 0..height)
|
|
59
|
+ {
|
|
60
|
+ auto p = Pos(x, y);
|
|
61
|
+ //this[p].type.tick(this, p);
|
|
62
|
+ }
|
|
63
|
+ }
|
|
64
|
+ }
|
|
65
|
+
|
34
|
66
|
void recalculateAccessibility()
|
35
|
67
|
{
|
36
|
68
|
foreach (x; 0..WIDTH)
|
37
|
69
|
{
|
38
|
70
|
foreach (y; 0..HEIGHT)
|
39
|
71
|
{
|
40
|
|
- grid[x][y].access = Access.none;
|
|
72
|
+ grid[x][y].access = Player.none;
|
41
|
73
|
}
|
42
|
74
|
}
|
43
|
|
- accessFrom(startA, Access.a);
|
44
|
|
- accessFrom(startB, Access.b);
|
|
75
|
+ accessFrom(startA, Player.a);
|
|
76
|
+ accessFrom(startB, Player.b);
|
45
|
77
|
}
|
46
|
78
|
|
47
|
|
- void accessFrom(Pos start, Access access)
|
|
79
|
+ void accessFrom(Pos start, Player access)
|
48
|
80
|
{
|
49
|
81
|
import std.container.rbtree;
|
50
|
82
|
auto queue = new RedBlackTree!Pos;
|
51
|
83
|
queue.insert(start);
|
52
|
84
|
bool[Pos] seen;
|
|
85
|
+ this[start].access |= access;
|
53
|
86
|
|
54
|
87
|
while (!queue.empty)
|
55
|
88
|
{
|
|
@@ -58,20 +91,30 @@ final class Board
|
58
|
91
|
foreach (offset; p.type.downstreamOffsets)
|
59
|
92
|
{
|
60
|
93
|
auto t = p.pos + offset;
|
|
94
|
+ if (!inBounds(t))
|
|
95
|
+ {
|
|
96
|
+ continue;
|
|
97
|
+ }
|
61
|
98
|
if (t in seen)
|
62
|
99
|
{
|
63
|
100
|
continue;
|
64
|
101
|
}
|
65
|
102
|
seen[t] = true;
|
66
|
103
|
auto bt = this[t];
|
67
|
|
- if (bt.type !is null && (bt.access & access) == Access.none)
|
|
104
|
+ if (bt.type !is null && (bt.access & access) == Player.none)
|
68
|
105
|
{
|
69
|
106
|
queue.insert(t);
|
70
|
107
|
}
|
|
108
|
+ this[t].access |= access;
|
71
|
109
|
}
|
72
|
110
|
}
|
73
|
111
|
}
|
74
|
112
|
|
|
113
|
+ bool inBounds(Pos pos)
|
|
114
|
+ {
|
|
115
|
+ return pos.x >= 0 && pos.y >= 0 && pos.x < width && pos.y < height;
|
|
116
|
+ }
|
|
117
|
+
|
75
|
118
|
ref Block opIndex(Pos pos)
|
76
|
119
|
{
|
77
|
120
|
return grid[pos.x][pos.y];
|
|
@@ -87,33 +130,43 @@ final class Board
|
87
|
130
|
}
|
88
|
131
|
}
|
89
|
132
|
}
|
|
133
|
+
|
|
134
|
+ auto blocks()
|
|
135
|
+ {
|
|
136
|
+ import std.range, std.algorithm;
|
|
137
|
+ return cartesianProduct(iota(width), iota(height))
|
|
138
|
+ .map!(x => grid[x[0]][x[1]]);
|
|
139
|
+ }
|
90
|
140
|
}
|
91
|
141
|
|
92
|
|
-@("Access for a fresh board")
|
|
142
|
+@("Player for a fresh board")
|
93
|
143
|
unittest
|
94
|
144
|
{
|
95
|
145
|
auto board = new Board;
|
96
|
|
- assert(board[board.startA].access == Access.a);
|
97
|
|
- assert(board[board.startB].access == Access.b);
|
|
146
|
+ assert(board[board.startA].access == Player.a);
|
|
147
|
+ assert(board[board.startB].access == Player.b);
|
98
|
148
|
|
99
|
149
|
// Some random points around that should be empty
|
100
|
|
- assert(board[Pos(1, 1)].access == Access.none);
|
101
|
|
- assert(board[Pos(20, 10)].access == Access.none);
|
102
|
|
- assert(board[Pos(0, 0)].access == Access.none);
|
|
150
|
+ assert(board[Pos(1, 1)].access == Player.none);
|
|
151
|
+ assert(board[Pos(20, 10)].access == Player.none);
|
|
152
|
+ assert(board[Pos(0, 0)].access == Player.none);
|
103
|
153
|
|
104
|
154
|
// The neighborhood near the start positions
|
105
|
155
|
foreach (dx; -1 .. 2)
|
106
|
156
|
{
|
107
|
157
|
foreach (dy; -1 .. 2)
|
108
|
158
|
{
|
109
|
|
- assert(board[Pos(board.startA.x + dx, board.startA.y + dy)].access == Access.a);
|
110
|
|
- assert(board[Pos(board.startB.x + dx, board.startB.y + dy)].access == Access.b);
|
|
159
|
+ auto pa = board.startA + Pos(dx, dy);
|
|
160
|
+ auto pb = board.startB + Pos(dx, dy);
|
|
161
|
+ assert(board[pa].access == Player.a, format("%s should have had access for player a", pa));
|
|
162
|
+ assert(board[pb].access == Player.b, format("%s should have had access for player b", pb));
|
|
163
|
+ assert(board[Pos(board.startB.x + dx, board.startB.y + dy)].access == Player.b);
|
111
|
164
|
}
|
112
|
165
|
}
|
113
|
|
- assert(board[Pos(0, 0)].access == Access.none);
|
|
166
|
+ assert(board[Pos(0, 0)].access == Player.none);
|
114
|
167
|
}
|
115
|
168
|
|
116
|
|
-enum Access
|
|
169
|
+enum Player
|
117
|
170
|
{
|
118
|
171
|
none = 0,
|
119
|
172
|
a = 0b01,
|
|
@@ -124,13 +177,29 @@ enum Access
|
124
|
177
|
struct Block
|
125
|
178
|
{
|
126
|
179
|
BlockType type;
|
127
|
|
- Access access;
|
128
|
180
|
Pos pos;
|
|
181
|
+ Player access;
|
|
182
|
+ Player placed;
|
|
183
|
+ uint turn;
|
|
184
|
+
|
|
185
|
+ void play(Player player, BlockType type, uint turn)
|
|
186
|
+ {
|
|
187
|
+ this.placed |= player;
|
|
188
|
+ this.type = type;
|
|
189
|
+ this.turn = turn;
|
|
190
|
+ }
|
|
191
|
+
|
|
192
|
+ void tick()
|
|
193
|
+ {
|
|
194
|
+ if (type !is null) type.tick(this);
|
|
195
|
+ }
|
129
|
196
|
|
130
|
197
|
void draw(SvgSink sink)
|
131
|
198
|
{
|
132
|
|
- string color;
|
133
|
|
- final switch (access) with (Access)
|
|
199
|
+ string color = sink.colorBg;
|
|
200
|
+ string strokeColor = sink.colorGrid;
|
|
201
|
+ double strokeWidth = sink.thinStroke;
|
|
202
|
+ final switch (access) with (Player)
|
134
|
203
|
{
|
135
|
204
|
case none:
|
136
|
205
|
color = sink.colorBg;
|
|
@@ -145,12 +214,37 @@ struct Block
|
145
|
214
|
color = sink.colorB;
|
146
|
215
|
break;
|
147
|
216
|
}
|
|
217
|
+ switch (placed) with (Player)
|
|
218
|
+ {
|
|
219
|
+ case a:
|
|
220
|
+ strokeColor = sink.colorBoldA;
|
|
221
|
+ strokeWidth = sink.mediumStroke;
|
|
222
|
+ break;
|
|
223
|
+ case b:
|
|
224
|
+ strokeColor = sink.colorBoldB;
|
|
225
|
+ strokeWidth = sink.mediumStroke;
|
|
226
|
+ break;
|
|
227
|
+ default:
|
|
228
|
+ strokeColor = sink.colorGrid;
|
|
229
|
+ strokeWidth = sink.thinStroke;
|
|
230
|
+ break;
|
|
231
|
+ }
|
148
|
232
|
auto center = sink.center(pos);
|
149
|
233
|
auto scale = Point(sink.halfScale, sink.halfScale);
|
150
|
234
|
auto left = center - Point(sink.halfScale, sink.halfScale);
|
151
|
|
- sink.put(format(`<rect x="%s" y="%s" width="%s" height="%s" fill="%s" stroke="#CAD0D1" stroke-width="%s" />`,
|
152
|
|
- left.x, left.y, sink.scale, sink.scale, color, sink.thinStroke));
|
|
235
|
+ if (placed != Player.none && placed != Player.both)
|
|
236
|
+ {
|
|
237
|
+ strokeWidth = sink.mediumStroke;
|
|
238
|
+ }
|
|
239
|
+ sink.put(format(`<rect x="%s" y="%s" width="%s" height="%s" fill="%s" stroke="%s" stroke-width="%s" />`,
|
|
240
|
+ left.x, left.y, sink.scale, sink.scale, color, strokeColor, strokeWidth));
|
153
|
241
|
if (type !is null) type.draw(sink, pos);
|
|
242
|
+ if (turn > 0)
|
|
243
|
+ {
|
|
244
|
+ sink.put(format(`<text x="%s" y="%s" font-size="%s" font-family="%s">%s</text>`,
|
|
245
|
+ left.x + 5, left.y + 20, sink.textSize, sink.font, turn));
|
|
246
|
+
|
|
247
|
+ }
|
154
|
248
|
}
|
155
|
249
|
}
|
156
|
250
|
|
|
@@ -161,6 +255,11 @@ abstract class BlockType
|
161
|
255
|
bool canCircle() @property { return circled != this; }
|
162
|
256
|
abstract const(Pos[]) downstreamOffsets();
|
163
|
257
|
|
|
258
|
+ void tick(ref Block block)
|
|
259
|
+ {
|
|
260
|
+ // Do nothing by default
|
|
261
|
+ }
|
|
262
|
+
|
164
|
263
|
static BlockType x;
|
165
|
264
|
static BlockType ox;
|
166
|
265
|
static BlockType plus;
|
|
@@ -168,6 +267,12 @@ abstract class BlockType
|
168
|
267
|
static BlockType star;
|
169
|
268
|
static BlockType ostar;
|
170
|
269
|
static BlockType source;
|
|
270
|
+ static BlockType collision1;
|
|
271
|
+ static BlockType collision2;
|
|
272
|
+ static BlockType collision3;
|
|
273
|
+ static BlockType collision4;
|
|
274
|
+ static BlockType collision5;
|
|
275
|
+ mixin Arrows!();
|
171
|
276
|
|
172
|
277
|
static BlockType[] playable;
|
173
|
278
|
|
|
@@ -180,18 +285,60 @@ abstract class BlockType
|
180
|
285
|
star = new BlockStar();
|
181
|
286
|
ostar = new BlockOStar();
|
182
|
287
|
source = new BlockSource;
|
|
288
|
+ collision1 = new Collision(1, null);
|
|
289
|
+ collision2 = new Collision(2, collision1);
|
|
290
|
+ collision3 = new Collision(3, collision2);
|
|
291
|
+ collision4 = new Collision(4, collision3);
|
|
292
|
+ collision5 = new Collision(5, collision4);
|
|
293
|
+
|
|
294
|
+ import std.string : startsWith;
|
183
|
295
|
|
184
|
|
- playable = [
|
185
|
|
- x, ox, plus, oplus, star, ostar,
|
186
|
|
- new Arrow(Pos(-1, -1)),
|
187
|
|
- new Arrow(Pos(-1, 0)),
|
188
|
|
- new Arrow(Pos(-1, 1)),
|
189
|
|
- new Arrow(Pos( 1, -1)),
|
190
|
|
- new Arrow(Pos( 1, 0)),
|
191
|
|
- new Arrow(Pos( 1, 1)),
|
192
|
|
- new Arrow(Pos( 0, 1)),
|
193
|
|
- new Arrow(Pos( 0, -1)),
|
194
|
|
- ];
|
|
296
|
+ static foreach (member; __traits(allMembers, BlockType))
|
|
297
|
+ {
|
|
298
|
+ static if (!member.startsWith("collision") && is(typeof(__traits(getMember, BlockType, member)) : BlockType))
|
|
299
|
+ {
|
|
300
|
+ static if (__traits(compiles, () { auto a = __traits(getMember, BlockType, member); }))
|
|
301
|
+ {
|
|
302
|
+ pragma(msg, "have type " ~ member);
|
|
303
|
+ playable ~= __traits(getMember, BlockType, member);
|
|
304
|
+ }
|
|
305
|
+ }
|
|
306
|
+ }
|
|
307
|
+ }
|
|
308
|
+}
|
|
309
|
+
|
|
310
|
+mixin template Arrows()
|
|
311
|
+{
|
|
312
|
+ import std.conv : to;
|
|
313
|
+ template name(int x, int y)
|
|
314
|
+ {
|
|
315
|
+ enum name = "arrow" ~ (["L", "", "R"][x + 1]) ~ (["U", "", "D"][y + 1]);
|
|
316
|
+ }
|
|
317
|
+
|
|
318
|
+ static foreach (dx; -1..2)
|
|
319
|
+ {
|
|
320
|
+ static foreach (dy; -1..2)
|
|
321
|
+ {
|
|
322
|
+ static if (dx != 0 || dy != 0)
|
|
323
|
+ {
|
|
324
|
+ mixin("static BlockType " ~ name!(dx, dy) ~ ";");
|
|
325
|
+ }
|
|
326
|
+ }
|
|
327
|
+ }
|
|
328
|
+
|
|
329
|
+ static this()
|
|
330
|
+ {
|
|
331
|
+ static foreach (dx; -1..2)
|
|
332
|
+ {
|
|
333
|
+ static foreach (dy; -1..2)
|
|
334
|
+ {
|
|
335
|
+ static if (dx != dy)
|
|
336
|
+ {
|
|
337
|
+ mixin(name!(dx, dy) ~ " = new Arrow(Pos(" ~
|
|
338
|
+ dx.to!string ~ ", " ~ dy.to!string ~ "));");
|
|
339
|
+ }
|
|
340
|
+ }
|
|
341
|
+ }
|
195
|
342
|
}
|
196
|
343
|
}
|
197
|
344
|
|
|
@@ -370,6 +517,54 @@ class Arrow : BlockType
|
370
|
517
|
}
|
371
|
518
|
}
|
372
|
519
|
|
|
520
|
+class Collision : BlockType
|
|
521
|
+{
|
|
522
|
+ uint stage;
|
|
523
|
+ BlockType next;
|
|
524
|
+ this(uint stage, BlockType next = null)
|
|
525
|
+ {
|
|
526
|
+ this.stage = stage;
|
|
527
|
+ this.next = next;
|
|
528
|
+ if (stage == 1)
|
|
529
|
+ {
|
|
530
|
+ assert(next is null);
|
|
531
|
+ }
|
|
532
|
+ else
|
|
533
|
+ {
|
|
534
|
+ assert(next !is null);
|
|
535
|
+ }
|
|
536
|
+ }
|
|
537
|
+
|
|
538
|
+ override void tick(ref Block block)
|
|
539
|
+ {
|
|
540
|
+ infof("Collision tick: %s -> %s", this, next);
|
|
541
|
+ block.type = next;
|
|
542
|
+ block.placed = Player.none;
|
|
543
|
+ block.turn = 0;
|
|
544
|
+ }
|
|
545
|
+
|
|
546
|
+ override string toString()
|
|
547
|
+ {
|
|
548
|
+ return format("Collision(%s)", stage);
|
|
549
|
+ }
|
|
550
|
+
|
|
551
|
+ override void draw(SvgSink sink, Pos pos)
|
|
552
|
+ {
|
|
553
|
+ // A half-sized square?
|
|
554
|
+ auto center = sink.center(pos);
|
|
555
|
+ auto scale = sink.halfScale * 0.3;
|
|
556
|
+ sink.put(format(
|
|
557
|
+ `<rect x="%s" y="%s" width="%s" height="%s" stroke="black" stroke-width="%s" />`,
|
|
558
|
+ center.x - scale, center.y - scale, scale * 2,
|
|
559
|
+ scale * 2, sink.strokeWidth));
|
|
560
|
+ sink.put(format(
|
|
561
|
+ `<text x="%s" y="%s" fill="white" font-size="%s" font-family="%s">%s</text>`,
|
|
562
|
+ center.x - 5, center.y + 5, sink.textSize, sink.font, stage));
|
|
563
|
+ }
|
|
564
|
+
|
|
565
|
+ override const(Pos[]) downstreamOffsets() { return null; }
|
|
566
|
+}
|
|
567
|
+
|
373
|
568
|
class BlockSource : BlockType
|
374
|
569
|
{
|
375
|
570
|
override void draw(SvgSink sink, Pos pos)
|
|
@@ -382,7 +577,7 @@ class BlockSource : BlockType
|
382
|
577
|
}
|
383
|
578
|
override const(Pos[]) downstreamOffsets()
|
384
|
579
|
{
|
385
|
|
- return BlockOStar.offsets;
|
|
580
|
+ return BlockStar.offsets;
|
386
|
581
|
}
|
387
|
582
|
}
|
388
|
583
|
|