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);