Browse Source

Added some AIs!

Also added collision blocks.
dhasenan 5 years ago
parent
commit
9d5c6976be
10 changed files with 552 additions and 47 deletions
  1. 1
    0
      dub.sdl
  2. 6
    0
      dub.selections.json
  3. 99
    4
      source/app.d
  4. 40
    0
      source/cosmic/ai.d
  5. 5
    0
      source/cosmic/base.d
  6. 229
    34
      source/cosmic/blocks.d
  7. 17
    9
      source/cosmic/drawing.d
  8. 30
    0
      source/cosmic/greedy.d
  9. 37
    0
      source/cosmic/random.d
  10. 88
    0
      source/cosmic/simple.d

+ 1
- 0
dub.sdl View File

@@ -3,3 +3,4 @@ description "An AI for CosmicBlocks"
3 3
 authors "dhasenan"
4 4
 copyright "Copyright © 2018, dhasenan"
5 5
 license "MS-PL"
6
+dependency "vectorflow" version="1.0.0"

+ 6
- 0
dub.selections.json View File

@@ -0,0 +1,6 @@
1
+{
2
+	"fileVersion": 1,
3
+	"versions": {
4
+		"vectorflow": "1.0.0"
5
+	}
6
+}

+ 99
- 4
source/app.d View File

@@ -1,18 +1,113 @@
1 1
 module app;
2 2
 
3
+import cosmic.ai;
3 4
 import cosmic.base;
4 5
 import cosmic.blocks;
5 6
 import cosmic.drawing;
7
+import cosmic.random;
8
+import cosmic.simple;
9
+
6 10
 import std.stdio;
7 11
 
8 12
 void main()
9 13
 {
10 14
     auto board = new Board;
11
-    foreach (i, type; BlockType.playable)
12
-    {
13
-        board[Pos(cast(int)i, 0)].type = type;
14
-    }
15
+    board.runGame(new Simple, new Simple);
16
+    //mockup(board);
15 17
     auto svg = new SvgSink();
16 18
     board.draw(svg);
17 19
     svg.write("board.svg");
18 20
 }
21
+
22
+void mockup(Board board)
23
+{
24
+    auto y = board.startA.y;
25
+    for (int i = board.startA.x + 2; i <= board.startB.x - 2; i++)
26
+    {
27
+        board[Pos(i, y)].type = BlockType.plus;
28
+    }
29
+    board[Pos(board.startA.x + 1, y)].type = BlockType.arrowR;
30
+    board[Pos(board.startB.x - 1, y)].type = BlockType.arrowL;
31
+    board.recalculateAccessibility;
32
+}
33
+
34
+void runGame(Board board, AI aia, AI aib)
35
+{
36
+    board.recalculateAccessibility;
37
+    foreach (block; board.blocks)
38
+    {
39
+        if (block.access != Player.none)
40
+        {
41
+            writefln("block %s accessible to %s", block.pos, block.access);
42
+        }
43
+    }
44
+    aia.init(board, Player.a);
45
+    aib.init(board, Player.b);
46
+    uint moves = 0;
47
+    uint collisions = 0;
48
+    while (true)
49
+    {
50
+        moves++;
51
+        if (moves > 300 || collisions > 10)
52
+        {
53
+            writefln("draw due to too many moves");
54
+            break;
55
+        }
56
+        auto ma = aia.nextMove();
57
+        writefln("Player A plays: %s", ma);
58
+        if (ma.forfeit)
59
+        {
60
+            writeln("Player A forfeits");
61
+            break;
62
+        }
63
+        assert(board[ma.pos].type is null, "tried to overwrite existing");
64
+        if (board.winners != Player.none)
65
+        {
66
+            writefln("winner: %s", board.winners);
67
+            break;
68
+        }
69
+
70
+        auto mb = aib.nextMove();
71
+        writefln("Player B plays: %s", mb);
72
+        if (mb.forfeit)
73
+        {
74
+            writeln("Player B forfeits");
75
+            break;
76
+        }
77
+        assert(board[mb.pos].type is null, "tried to overwrite existing");
78
+
79
+        if (mb.pos == ma.pos)
80
+        {
81
+            writefln("collision!");
82
+            board[ma.pos].type = BlockType.collision5;
83
+            collisions++;
84
+        }
85
+        else
86
+        {
87
+            collisions = 0;
88
+            board[ma.pos].play(Player.a, ma.type, moves);
89
+            board[mb.pos].play(Player.b, mb.type, moves);
90
+        }
91
+        board.tick;
92
+        if (board.winners != Player.none)
93
+        {
94
+            writefln("winner: %s", board.winners);
95
+            break;
96
+        }
97
+    }
98
+}
99
+
100
+Player winners(Board board)
101
+{
102
+    board.recalculateAccessibility;
103
+    Player w = Player.none;
104
+    if (board[board.startA].access & Player.b)
105
+    {
106
+        w |= Player.b;
107
+    }
108
+    if (board[board.startB].access & Player.a)
109
+    {
110
+        w |= Player.a;
111
+    }
112
+    return w;
113
+}

+ 40
- 0
source/cosmic/ai.d View File

@@ -1 +1,41 @@
1 1
 module cosmic.ai;
2
+
3
+import cosmic.base;
4
+import cosmic.blocks;
5
+import std.experimental.logger;
6
+
7
+struct Move
8
+{
9
+    static Move doForfeit = {forfeit: true};
10
+    Pos pos;
11
+    BlockType type;
12
+    bool forfeit = false;
13
+}
14
+
15
+abstract class AI
16
+{
17
+    Board board;
18
+    Player player;
19
+    Pos start, goal;
20
+    BlockType[] inventory;  // TODO make multiset
21
+
22
+    void init(Board board, Player player)
23
+    {
24
+        this.board = board;
25
+        this.player = player;
26
+        if (player == Player.a)
27
+        {
28
+            start = board.startA;
29
+            goal = board.startB;
30
+        }
31
+        else
32
+        {
33
+            start = board.startB;
34
+            goal = board.startA;
35
+        }
36
+        // Close enough?
37
+        this.inventory = BlockType.playable ~ BlockType.playable;
38
+    }
39
+
40
+    abstract Move nextMove();
41
+}

+ 5
- 0
source/cosmic/base.d View File

@@ -21,6 +21,11 @@ struct Pos
21 21
                 mixin("this.x " ~ op ~ " other.x"),
22 22
                 mixin("this.y " ~ op ~ " other.y"));
23 23
     }
24
+
25
+    int dist(Pos other)
26
+    {
27
+        return (y - other.y) ^^ 2 + (x - other.x) ^^ 2;
28
+    }
24 29
 }
25 30
 
26 31
 struct Point

+ 229
- 34
source/cosmic/blocks.d View File

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

+ 17
- 9
source/cosmic/drawing.d View File

@@ -53,18 +53,26 @@ class SvgSink
53 53
 
54 54
 class Theme
55 55
 {
56
-    double scale = 40;
56
+    double scale = 80;
57 57
     double halfScale() @property pure { return scale / 2; }
58 58
     double angleScale() @property pure { return (scale / 2) / (2 ^^ 0.5); }
59 59
 
60 60
     double drawablePortion = 0.8;
61 61
 
62
-    double strokeWidth = 4;
63
-    double thinStroke = 1;
64
-
65
-    string colorA = "#ff0000",
66
-           colorB = "#0000ff",
67
-           colorBoth = "#ff00ff",
68
-           colorBg = "#E3EAEA",
69
-           colorGrid = "#CAD0D1";
62
+    double strokeWidth = 8;
63
+    double thinStroke = 3;
64
+    double mediumStroke = 5;
65
+
66
+    double textSize = 20;
67
+    string font = "sans-serif";
68
+
69
+    // these HSV values are from some weird scale from 0 to 343
70
+    string colorA = "#e58f7b"  // HSV(11, 46, 90)
71
+         , colorB = "#7baee5"  // HSV(211, 46, 90)
72
+         , colorBoth = "#e57be5"  // HSV(300, 46, 90)
73
+         , colorBg = "#efe6d0"
74
+         , colorGrid = "#cad0d1"
75
+         , colorBoldA = "#e53c16" // HSV(11, 90, 90)
76
+         , colorBoldB = "#167ae5"  // HSV(211, 90, 90)
77
+           ;
70 78
 }

+ 30
- 0
source/cosmic/greedy.d View File

@@ -0,0 +1,30 @@
1
+module cosmic.greedy;
2
+
3
+import cosmic.ai;
4
+import cosmic.base;
5
+import cosmic.blocks;
6
+import std.random;
7
+
8
+class Greedy : AI
9
+{
10
+    private static Mt19937 rnd;
11
+
12
+    static this()
13
+    {
14
+        rnd.seed(1);
15
+    }
16
+
17
+    private Board board;
18
+
19
+    override void init(Board board, Player player)
20
+    {
21
+        this.board = board;
22
+    }
23
+
24
+    override Move nextMove()
25
+    {
26
+        // The value of a position is how valuable it is to get there.
27
+        //
28
+        assert(false);
29
+    }
30
+}

+ 37
- 0
source/cosmic/random.d View File

@@ -1 +1,38 @@
1 1
 module cosmic.random;
2
+
3
+import cosmic.ai;
4
+import cosmic.base;
5
+import cosmic.blocks;
6
+import std.random;
7
+
8
+class Random : AI
9
+{
10
+    private static Mt19937 rnd;
11
+
12
+    static this()
13
+    {
14
+        rnd.seed(1);
15
+    }
16
+
17
+    private Board board;
18
+
19
+    override void init(Board board, Player player)
20
+    {
21
+        this.board = board;
22
+    }
23
+
24
+    override Move nextMove()
25
+    {
26
+        foreach (attempt; 0..10)
27
+        {
28
+            auto x = uniform(0, board.width, rnd);
29
+            auto y = uniform(0, board.height, rnd);
30
+            auto p = Pos(x, y);
31
+            if (board[p].type is null)
32
+            {
33
+                return Move(p, choice(BlockType.playable, rnd), false);
34
+            }
35
+        }
36
+        return Move(Pos(0, 0), null, true);
37
+    }
38
+}

+ 88
- 0
source/cosmic/simple.d View File

@@ -0,0 +1,88 @@
1
+module cosmic.simple;
2
+
3
+import cosmic.ai;
4
+import cosmic.base;
5
+import cosmic.blocks;
6
+import std.experimental.logger;
7
+import std.random;
8
+
9
+/**
10
+  * A simple AI that tries to make moves that directly contribute to victory.
11
+  * It is unsubtle and relatively obvious.
12
+  */
13
+class Simple : AI
14
+{
15
+    private static Mt19937 rnd;
16
+
17
+    static this()
18
+    {
19
+        rnd.seed(1);
20
+    }
21
+
22
+    override Move nextMove()
23
+    {
24
+        // There are two types of moves in a single-player mindset:
25
+        // 1. Closing a gap
26
+        // 2. Increasing the options
27
+        // We can often do both at once. However, we might sometimes do only one or the other.
28
+        if (rnd.dice(0.8, 0.2) == 0)
29
+        {
30
+            return closeGap;
31
+        }
32
+        else
33
+        {
34
+            return increaseOptions;
35
+        }
36
+    }
37
+
38
+    Move increaseOptions()
39
+    {
40
+        // Don't know how
41
+        return closeGap;
42
+    }
43
+
44
+    Move closeGap()
45
+    in
46
+    {
47
+        assert(player != Player.none, "AI must have a player");
48
+    }
49
+    do
50
+    {
51
+        // Find the open position closest to your opponent's position
52
+        import std.algorithm, std.array;
53
+        infof("close gap for player %s", player);
54
+        auto options = board.blocks
55
+            .filter!(x => (x.access & player) == player)
56
+            .filter!(x => x.type is null)
57
+            .array
58
+            .sort!((a, b) => a.pos.dist(goal) < b.pos.dist(goal));
59
+        if (options.length == 0)
60
+        {
61
+            // TODO try to use a remove
62
+            info("failed to find a move: no open spaces");
63
+            return Move.doForfeit;
64
+        }
65
+        foreach (option; options)
66
+        {
67
+            foreach (type; inventory)
68
+            {
69
+                bool getsCloser = false;
70
+                foreach (offset; type.downstreamOffsets)
71
+                {
72
+                    if (goal.dist(option.pos + offset) < goal.dist(option.pos))
73
+                    {
74
+                        getsCloser = true;
75
+                        break;
76
+                    }
77
+                }
78
+                if (!getsCloser) continue;
79
+                // Let's just take the first.
80
+                Move m = {pos: option.pos, type: type};
81
+                return m;
82
+            }
83
+        }
84
+        info("nothing gets us closer; doing something random");
85
+        Move m = {pos: options[0].pos, type: inventory[0]};
86
+        return m;
87
+    }
88
+}