1 module board;
2 
3 import core.atomic : atomicStore;
4 import std.concurrency : spawn, thisTid, Tid, send, receive;
5 
6 import dlangui;
7 
8 import dreversi.agent;
9 import dreversi.env : Board, reset, P = Point, put, pass;
10 
11 // helper for scaling relative to average 96dpi FullHD, IDK but maybe a bad idea after all
12 T scaledByDPI(T)(T val)
13 {
14     return val *= (SCREEN_DPI() / cast(T)96);
15 }
16 
17 struct CancelMessage {}
18 struct CancelAckMessage {}
19 
20 void worker(Tid parent, bool isBlack, int depth, shared(bool)* _agentDone)
21 {
22     auto agent = AlphaBetaAgent(isBlack, true);
23     bool canceled = false;
24     Log.d("agent worker started at ", thisTid);
25 
26     while (!canceled)
27     {
28         receive(
29             (immutable Board env)
30             {
31                 Log.d("agent received board");
32                 if (pass(env, agent.isBlack)) {
33                     Log.d("skip agent turn");
34                     send(parent, env);
35                 } else {
36                     const action = agent.select(env, depth);
37                     immutable next = put(env, agent.isBlack, action.row, action.col);
38                     Log.d("agent move: ", [action.row, action.col]);
39                     send(parent, next);
40                     Log.d(next);
41                 }
42                 atomicStore(*_agentDone, true);
43             },
44             (CancelMessage m)
45             {
46                 send(parent, CancelAckMessage());
47                 canceled = true;
48             }
49             );
50     }
51 }
52 
53 class BoardWidget : CanvasWidget
54 {
55     const int rows, cols;
56     const int margin;
57     const float lineWidth;
58     const bool isBlack;
59 
60     Board board;
61     Tid agentTid;
62     bool _isPlayerTurn;
63 
64     shared bool _agentDone = false;
65 
66     this(int rows, int cols, int depth, bool playFirst = true)
67     {
68         super("board-widget");
69 
70         this.rows = rows;
71         this.cols = cols;
72         this._isPlayerTurn = playFirst;
73         this.isBlack = playFirst;
74 
75         this.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
76         this.margin = 100.scaledByDPI;
77         this.lineWidth = 3.scaledByDPI;
78         super.minHeight = 400.scaledByDPI;
79         this.clickable(true);
80         this.board = reset(rows, cols);
81         this.agentTid = spawn(&worker, thisTid, !this.isBlack, depth, &this._agentDone);
82 
83         this.mouseEvent = delegate bool(Widget w1, MouseEvent e)
84             {
85                 if (e.lbutton.isDown)
86                 {
87                     const rc = xy2rc(e.x, e.y);
88                     const move = rc.int2;
89                     Log.d("clicked at ", move);
90                     if (this._isPlayerTurn)
91                     {
92                         if (valid(rc))
93                         {
94                             immutable next = board.put(this.isBlack, move[1], move[0]);
95                             if (next.valid)
96                             {
97                                 this.board = next;
98                                 this._agentDone.atomicStore(false);
99                                 this._isPlayerTurn = false;
100                                 send(this.agentTid, next);
101                             }
102                         }
103                     }
104                 }
105                 return true;
106             };
107     }
108 
109     ~this()
110     {
111         import std.concurrency : receiveOnly;
112 
113         Log.d("waiting for agent thread closed");
114         send(this.agentTid, CancelMessage());
115         // receiveOnly!CancelAckMessage;
116     }
117 
118     struct RC
119     {
120         float row, col;
121 
122         int[2] int2() const { return [cast(int) row, cast(int) col]; }
123     }
124 
125     auto valid(RC rc)
126     {
127         return 0 <= rc.row && rc.row < this.rows && 0 <= rc.col && rc.col < this.cols;
128     }
129 
130     auto xy2rc(int x, int y)
131     {
132         const rc = shrinkToBoard(this.pos);
133         const width = rc.right - rc.left;
134         const height = rc.bottom - rc.top;
135         const sx = width / this.cols;
136         const sy = height / this.rows;
137         return RC(cast(float) (x - rc.left) / sx,
138                   cast(float) (y - rc.top) / sy);
139     }
140 
141     override protected void measuredContent(
142         int parentWidth, int parentHeight,
143         int contentWidth, int contentHeight)
144     {
145         import std.algorithm.comparison;
146         _measuredHeight = max(minHeight, min(contentHeight, contentWidth));
147         _measuredWidth = _measuredHeight; // max(minHeight, contentWidth);
148     }
149 
150     protected void drawText(DrawBuf buf, Rect rc, dstring text)
151     {
152         FontRef font = font();
153         Point sz = font.textSize(text);
154         applyAlign(rc, sz, Align.HCenter, Align.Bottom );
155         font.drawText(buf, rc.left, rc.top, text, textColor, 4, 0, textFlags);
156     }
157 
158     // shrink to square with margin
159     Rect shrinkToBoard(Rect rc)
160     {
161         const _width = rc.right - rc.left;
162         const _height = rc.bottom - rc.top;
163         if (_width > _height)
164         {
165             rc.shrink((_width - height) / 2, 0);
166         }
167         else
168         {
169             rc.shrink(0, (_height - _width) / 2);
170         }
171         rc.shrink(this.margin, this.margin);
172         return rc;
173     }
174 
175     override void doDraw(DrawBuf buf, Rect _rc)
176     {
177         if (!this._isPlayerTurn && this._agentDone)
178     
179         {
180             receive(
181                 (const Board b) {
182                     this.board = b;
183                     this._isPlayerTurn = true;
184                 });
185         }
186 
187         import std.conv : to;
188 
189         const rc = shrinkToBoard(_rc);
190 
191         // draw background
192         buf.fillRect(rc, 0x119911);
193 
194         // draw row/col numbered lines
195         const width = rc.right - rc.left;
196         const height = rc.bottom - rc.top;
197         const sx = cast(float) width / this.cols;
198         const sy = cast(float) height / this.rows;
199         const fy = cast(int) (sy * 0.4);
200         foreach (r; 0 .. this.rows + 1)
201         {
202             const y = rc.top + sy * r + this.lineWidth / 2;
203             if (r < this.rows)
204             {
205                 const fontRc = Rect(rc.left - fy, cast(int) y + fy,
206                                     rc.left, cast(int) y + fy * 2);
207                 this.drawText(buf, fontRc, r.to!dstring);
208             }
209             buf.drawLineF(PointF(rc.left, y),
210                           PointF(rc.right, y),
211                           lineWidth,
212                           Color.black);
213         }
214         foreach (c; 0 .. this.cols + 1)
215         {
216             const x = cast(float) rc.left + sx * c + this.lineWidth / 2.0;
217             if (c < this.cols)
218             {
219                 const fontRc = Rect(cast(int) x + fy, rc.top + fy,
220                                     cast(int) x + fy * 2, rc.top);
221                 this.drawText(buf, fontRc, c.to!dstring);
222             }
223             buf.drawLineF(PointF(x, rc.top),
224                           PointF(x, rc.bottom),
225                           lineWidth,
226                           Color.black);
227         }
228 
229         // draw circles
230         assert(!board.empty, "Error: board is not initialized");
231         const rmargin = 5.scaledByDPI;
232         const rx = sx / 2 - rmargin;
233         const ry = sy / 2 - rmargin;
234         foreach (r; 0 .. this.rows)
235         {
236             foreach (c; 0 .. this.cols)
237             {
238                 auto p = this.board[r, c];
239                 if (p != P.empty)
240                 {
241                     buf.drawEllipseF(rc.left + c * sx + rx + rmargin,
242                                      rc.top + r * sy + ry + rmargin,
243                                      rx, ry, 0, Color.black,
244                                      p == P.black ? Color.black : Color.white);
245                 }
246             }
247         }
248     }
249 }