1 /++
2     Implementation of the "Sea Battle" game on the "Tida" framework using the OOP structure.
3     
4     At the moment, only one game can be played alone with a bot. The bot can make moves, 
5     and if it finds a ship, it finds adjacent decks until it completely destroys it.
6     
7     Win or lose is determined by the background. If green - the player won, red - the computer.
8     
9     To restart the game, press the "R" button and the game will clear the field and randomly generate both cards.
10     
11     Authors: $(HTTP https://github.com/TodNaz, TodNaz)
12     License: $(HTTP https://opensource.org/licenses/MIT, MIT)
13 +/
14 module seabattle;
15 
16 import tida;
17 
18 immutable CellStateSize = 16;
19 
20 enum CellState
21 {
22     Empty,
23     Missed,
24     Wounded,
25     ShipUnite
26 }
27 
28 void generateFieldGrid(ref CellState[10][10] grid) @safe
29 {
30     import 	std.random,
31             std.algorithm : canFind;
32 
33     bool isEmptyUnite(int x, int y) {
34         bool[] trace;
35 
36         trace ~= grid[x][y] == CellState.Empty;
37         if(x - 1 >= 0) trace ~= grid[x - 1][y] == CellState.Empty;
38         if(x + 1 < 10) trace ~= grid[x + 1][y] == CellState.Empty;
39         if(y - 1 >= 0) trace ~= grid[x][y - 1] == CellState.Empty;
40         if(y + 1 < 10) trace ~= grid[x][y + 1] == CellState.Empty;
41 
42         return !trace.canFind(false);
43     }
44 
45     void genShip(int size)
46     {
47         if(uniform(0, 2) == 1)
48         {
49             int posX = uniform(0, 10 - size);
50             int posY = uniform(0, 10);
51  
52             for(int i = posX; i < posX + size; i++) { 
53                  if(!isEmptyUnite(i, posY)) {
54                     genShip(size);
55                     return;
56                 }
57             }
58  
59             for(int i = posX; i < posX + size; i++) grid[i][posY] = CellState.ShipUnite;
60         }else
61         {
62             int posX = uniform(0, 10);
63             int posY = uniform(0, 10 - size);
64  
65             for(int i = posY; i < posY + size; i++) {
66                 if(!isEmptyUnite(posX, i)) {
67                     genShip(size);
68                     return;
69                 }
70             }
71  
72             for(int i = posY; i < posY + size; i++) grid[posX][i] = CellState.ShipUnite;
73         }
74     }
75  
76     foreach(_; 0 .. 2) genShip(4);
77     foreach(_; 0 .. 3) genShip(3);
78     foreach(_; 0 .. 4) genShip(2);
79     foreach(_; 0 .. 5) genShip(1);
80 }
81 
82 void clearFieldGrid(ref CellState[10][10] grid) @safe
83 {
84     for(int y = 0; y < grid.length; y++)
85     {
86         for(int x; x < grid[0].length; x++)
87         {
88             grid[x][y] = CellState.Empty;
89         }
90     }
91 }
92 
93 class PlayingField : Instance
94 {
95     public
96     {
97         CellState[10][10] grid;
98         Symbol[] numerateVertical;
99         Symbol[] numerateHorizontal;
100         bool isVisibleShip = true;
101         Vecf mousePosition = vecfNaN;
102     }
103 
104     this(Font font) @safe
105     {
106         numerateVertical = new Text(font).toSymbols("0123456789", rgb(0, 0, 0));
107         numerateHorizontal = new Text(font).toSymbols("abcdefghij", rgb(0, 0, 0));
108         generateFieldGrid(grid);
109     }
110 
111     @Event!Input
112     void onEvent(EventHandler event) @safe
113     {
114         mousePosition = vecf(event.mousePosition[0], event.mousePosition[1]);
115     }
116 
117     CellState mark(int posX, int posY) @safe
118     {
119         auto state = grid[posX][posY];
120         if(state == CellState.Empty) {
121             grid[posX][posY] = CellState.Missed;
122             return CellState.Missed;
123         } else
124         if(state == CellState.ShipUnite) {
125             grid[posX][posY] = CellState.Wounded;
126             return CellState.Wounded;
127         } else
128             return grid[posX][posY];
129     }
130 
131     bool isMouseHoverUniteGrid(int x, int y) @safe
132     {
133         return  mousePosition.x > position.x + (x * CellStateSize) &&
134                 mousePosition.y > position.y + (y * CellStateSize) &&
135                 mousePosition.x < position.x + (x * CellStateSize) + CellStateSize &&
136                 mousePosition.y < position.y + (y * CellStateSize) + CellStateSize;   
137     }
138 
139     @Event!Draw
140     void draw(IRenderer render) @safe
141     {	
142         for(int y = 0; y < grid.length; y++)
143         {
144             render.drawEx(	numerateVertical[y].image, 
145             				position + Vecf(-12, y * CellStateSize),
146             				0.0f,
147             				vecfNaN,
148             				vecfNaN,
149             				255,
150             				rgb(0, 0, 0)); 
151             render.drawEx(	numerateHorizontal[y].image, 
152             				position + Vecf(y * CellStateSize, -6) - numerateHorizontal[y].position,
153             				0.0f,
154             				vecfNaN,
155             				vecfNaN,
156             				255,
157             				rgb(0, 0, 0));
158             for(int x; x < grid[0].length; x++)
159             {
160                 
161                 if(isMouseHoverUniteGrid(x, y)) {
162                     render.rectangle(position + Vecf(x * CellStateSize, y * CellStateSize),
163                          CellStateSize, CellStateSize, rgb(200, 200, 200), true);
164                     render.rectangle(position + Vecf(x * CellStateSize, y * CellStateSize),
165                          CellStateSize, CellStateSize, rgb(255, 0, 0), false);
166                 } else
167                 {
168                     render.rectangle(position + Vecf(x * CellStateSize, y * CellStateSize),
169                          CellStateSize, CellStateSize, rgb(0, 0, 0), false);
170                 }
171 
172                 if(grid[x][y] != CellState.Empty) {
173                     auto state = grid[x][y];
174                     if(grid[x][y] == CellState.Missed) {
175                         render.circle(	position + Vecf(x * CellStateSize + (CellStateSize / 2), y * CellStateSize + (CellStateSize / 2)),
176                                 CellStateSize / 2, rgb(0, 0, 0), true);
177                     } else
178                     if(grid[x][y] == CellState.Wounded) {
179                         render.line( [	position + Vecf(x * CellStateSize, y * CellStateSize),
180                                 position + Vecf(x * CellStateSize + CellStateSize, y * CellStateSize + CellStateSize) ],
181                                 rgb(0,0,0));
182                         render.line( [	position + Vecf(x * CellStateSize + CellStateSize, y * CellStateSize),
183                                 position + Vecf(x * CellStateSize, y * CellStateSize + CellStateSize) ],
184                                 rgb(0,0,0));
185                     } else
186                     if(grid[x][y] == CellState.ShipUnite && isVisibleShip) {
187                         render.rectangle(	position + Vecf(x * CellStateSize, y * CellStateSize),
188                                     CellStateSize, CellStateSize, rgb(0, 0, 0), true);
189                     }
190                 }
191             }
192         }
193     }
194 }
195 
196 class Bot
197 {
198     public
199     {
200         PlayingField playerField;
201         PlayingField selfField;
202 
203         CellState[10][10] marked;
204     }
205 
206     this(PlayingField playerField, PlayingField selfField) @safe
207     {
208         this.playerField = playerField;
209         this.selfField = selfField;
210     }
211 
212     enum Side { none, left, right, up, down };
213 
214     private
215     {
216         Side currentSide = Side.none; 
217         int lastPosX = 0;
218         int lastPosY = 0;
219         bool lastSucces = false;
220 
221         int wordedX = -1;
222         int wordedY = -1;
223 
224         Side[4] lastSides;
225         int currSideI = 0;
226     }
227 
228     void clear() @safe
229     {
230         currentSide = Side.none;
231         lastPosX = 0;
232         lastPosY = 0;
233         lastSucces = false;
234         wordedX = -1;
235         wordedY = -1;
236         lastSides[0 .. 4] = Side.none;
237         currSideI = 0;
238         clearFieldGrid(marked);
239     }
240 
241     void makeAMove() @safe
242     {
243         import std.random : uniform;
244         import std.algorithm : canFind;
245 
246         int posX, posY;
247 
248         bool isMarkedEmpty(int x, int y) { 
249             if(x >= 0 && x < 10 && y >= 0 && y < 10) 
250                 return marked[x][y] == CellState.Empty; 
251             else 
252                 return false; 
253         }
254 
255         if(currentSide == Side.none) {
256             posX = uniform(0, 10);
257             posY = uniform(0, 10);
258         } else
259         if(currentSide == Side.up) {
260             posX = lastPosX;
261             posY = lastPosY - 1;
262         } else
263         if(currentSide == Side.down) {
264             posX = lastPosX;
265             posY = lastPosY + 1;
266         } else
267         if(currentSide == Side.left) {
268             posX = lastPosX - 1;
269             posY = lastPosY;
270         } else
271         if(currentSide == Side.right) {
272             posX = lastPosX + 1;
273             posY = lastPosY;
274         }
275 
276         if(isMarkedEmpty(posX, posY))
277         {
278             auto last = playerField.mark(posX, posY);
279             marked[posX][posY] = last;
280 
281             if(last == CellState.Wounded) {
282                 if(!lastSucces)
283                 {
284                     if(isMarkedEmpty(posX, posY - 1))
285                     {
286                         currentSide = Side.up;
287                         lastPosX = posX;
288                         lastPosY = posY;
289                         lastSides[++currSideI] = currentSide;
290                     }else
291                     if(isMarkedEmpty(posX, posY + 1))
292                     {
293                         currentSide = Side.down;
294                         lastPosX = posX;
295                         lastPosY = posY;
296                         lastSides[++currSideI] = currentSide;
297                     }else
298                     if(isMarkedEmpty(posX - 1, posY))
299                     {
300                         currentSide = Side.left;
301                         lastPosX = posX;
302                         lastPosY = posY;
303                         lastSides[++currSideI] = currentSide;
304                     }else
305                     if(isMarkedEmpty(posX + 1, posY))
306                     {
307                         currentSide = Side.right;
308                         lastPosX = posX;
309                         lastPosY = posY;
310                         lastSides[++currSideI] = currentSide;
311                     }
312 
313                     wordedX = posX;
314                     wordedY = posY;
315 
316                     lastSucces = true;
317                 }else
318                 {
319                     switch(currentSide)
320                     {
321                         case Side.left:
322                             if(!isMarkedEmpty(posX - 1, posY)) {
323                                 lastPosX = wordedX;
324                                 lastPosY = wordedY;
325                                 lastSucces = false;
326                                 goto default;
327                             }
328                             break;
329 
330                         case Side.right:
331                             if(!isMarkedEmpty(posX + 1, posY)) {
332                                 lastPosX = wordedX;
333                                 lastPosY = wordedY;
334                                 lastSucces = false;
335                                 goto default;
336                             }
337                             break;
338 
339                         case Side.up:
340                             if(!isMarkedEmpty(posX, posY - 1)) {
341                                 lastPosX = wordedX;
342                                 lastPosY = wordedY;
343                                 lastSucces = false;
344                                 goto default;
345                             }
346                             break;
347 
348                         case Side.down:
349                             if(!isMarkedEmpty(posX, posY + 1)) {
350                                 lastPosX = wordedX;
351                                 lastPosY = wordedY;
352                                 lastSucces = false;
353                                 goto default;
354                             }
355                             break;
356 
357                         default:
358                             lastPosX = 0;
359                             lastPosY = 0;
360                             wordedX = -1;
361                             wordedY = -1;
362                             lastSucces = false;
363                             currentSide = Side.none;
364                             return;
365                     }
366 
367                     lastPosX = posX;
368                     lastPosY = posY;
369                 }
370             }else
371             {
372                 if(wordedX != -1) {
373                     bool ll = 0, lr = 0, lu = 0, ld = 0;
374                     foreach(i; 0 .. currSideI) {
375                         if(lastSides[i] == Side.up) lu = 1; else
376                         if(lastSides[i] == Side.down) ld = 1; else
377                         if(lastSides[i] == Side.left) ll = 1; else
378                         if(lastSides[i] == Side.right) lr = 1; 
379                     }
380 
381                     if(!ll && isMarkedEmpty(wordedX - 1, wordedY)) { 
382                         currentSide = Side.left;
383                         lastPosX = wordedX;
384                         lastPosX = wordedY;
385                         return;
386                     }else
387                     if(!lr && isMarkedEmpty(wordedX + 1, wordedY)) {
388                         currentSide = Side.right;
389                         lastPosX = wordedX;
390                         lastPosX = wordedY;
391                         return;
392                     }else
393                     if(!lu && isMarkedEmpty(wordedX, wordedY - 1)) {
394                         currentSide = Side.up;
395                         lastPosX = wordedX;
396                         lastPosX = wordedY;
397                         return;
398                     }else
399                     if(!ld && isMarkedEmpty(wordedX, wordedY + 1)) {
400                         currentSide = Side.down;
401                         lastPosX = wordedX;
402                         lastPosX = wordedY;
403                         return;
404                     }else
405                     {
406                         currentSide = Side.none;
407                         wordedX = -1;
408                         wordedY = -1;
409                         lastSucces = 0;
410                         currSideI = 0;
411                         return;
412                     }
413                 }else {
414                     currentSide = Side.none;
415                     currSideI = 0;
416                 }
417             }
418         }else {
419             currentSide = Side.none;
420             wordedX = -1;
421             wordedY = -1;
422             lastSucces = 0;
423             currSideI = 0;
424             makeAMove();
425         }
426     }
427 }
428 
429 class SeaBattleMainScene : Scene
430 {
431     public
432     {
433         PlayingField playerField;
434         PlayingField targetField;
435         Font font;
436 
437         Bot bot;
438         bool isPlayerTurn = true;
439     }
440 
441     this() @safe
442     {
443         font = new Font().load("sans.ttf", 8);
444 
445         add(playerField = new PlayingField(font));
446         add(targetField = new PlayingField(font));
447 
448         playerField.position = vecf(128 - 32, 128 + 32);
449         targetField.position = vecf(320 + 64, 128 + 32);
450 
451         targetField.isVisibleShip = false;
452 
453         bot = new Bot(playerField, targetField);
454     }
455 
456     bool isWin(PlayingField field) @safe
457     {
458         for(int x = 0; x < 10; x++) {
459             for(int y = 0; y < 10; y++) {
460                 if(field.grid[x][y] == CellState.ShipUnite) return false;
461             }
462         } 
463 
464         return true;
465     }
466 
467     void checkWin() @safe
468     {
469         if(isWin(playerField)) {
470             renderer.background = rgb(255, 64, 64);
471         }
472         
473         if(isWin(targetField)) {
474             renderer.background = rgb(64, 255, 64);
475         }
476     }
477 
478     @Event!Input
479     void onEvent(EventHandler event) @safe
480     {
481         import std.random : uniform;
482         
483         if(isPlayerTurn) {
484             Vecf mousePosition = vecf(event.mousePosition[0], event.mousePosition[1]);
485             
486             if(event.mouseDownButton == MouseButton.left)
487             {
488                 if( mousePosition.x > targetField.position.x &&
489                     mousePosition.y > targetField.position.y)
490                 {
491                     for(int x = 0; x < 10; x++)
492                     {
493                         for(int y = 0; y < 10; y++)
494                         {
495                             if( mousePosition.x > targetField.position.x + x * CellStateSize &&
496                                 mousePosition.y > targetField.position.y + y * CellStateSize &&
497                                 mousePosition.x < targetField.position.x + x * CellStateSize + CellStateSize &&
498                                 mousePosition.y < targetField.position.y + y * CellStateSize + CellStateSize)
499                             {
500                                 targetField.mark(x, y);
501                                 isPlayerTurn = false;
502                                 checkWin();
503                                 
504                                 listener.timer({
505                                     bot.makeAMove();
506                                     isPlayerTurn = true;
507                                     checkWin();
508                                 }, dur!"msecs"(uniform(500, 2000)));
509                             }
510                         }
511                     }
512                 }
513             }
514         }
515         
516         if(event.keyDown == Key.R)
517         {
518             clearFieldGrid(playerField.grid);
519             clearFieldGrid(targetField.grid);
520             generateFieldGrid(playerField.grid);
521             generateFieldGrid(targetField.grid);
522             
523             bot.clear();
524         }
525     }
526 
527     debug @Event!Draw
528     void debugDraw(IRenderer render) @safe
529     {
530         import std.conv : to;
531 
532         render.draw(new Text(font).renderSymbols(fps.deltatime.to!string, rgb(0,0,0)), vecf(0,0));
533     }
534 }
535 
536 mixin GameRun!(GameConfig(640, 480, "Sea Battle"), SeaBattleMainScene);