battleship.go (14385B)
1 package interceptors 2 3 import ( 4 "bytes" 5 "dkforest/pkg/database" 6 dutils "dkforest/pkg/database/utils" 7 "dkforest/pkg/utils" 8 "dkforest/pkg/web/handlers/interceptors/command" 9 "encoding/base64" 10 "errors" 11 "fmt" 12 "github.com/fogleman/gg" 13 "github.com/sirupsen/logrus" 14 "html/template" 15 "image/color" 16 "strconv" 17 "sync" 18 "time" 19 ) 20 21 // Carrier 5 22 // Battleship 4 23 // Cruiser 3 24 // Submarine 3 25 // Destroyer 2 26 27 /** 28 ◀■■▶ 29 ▲ 30 █ 31 ▼ 32 ● 33 */ 34 35 var BattleshipInstance *Battleship 36 37 type BSCoordinate struct { 38 x, y int 39 } 40 41 type BSPlayer struct { 42 id database.UserID 43 username database.Username 44 userStyle string 45 card *BSCard 46 shots map[int]struct{} 47 } 48 49 func newPlayer(player database.User) *BSPlayer { 50 p := new(BSPlayer) 51 p.id = player.ID 52 p.username = player.Username 53 p.userStyle = player.GenerateChatStyle() 54 p.card = generateCard() 55 p.shots = make(map[int]struct{}) 56 return p 57 } 58 59 type Direction int 60 61 const ( 62 vertical Direction = iota + 1 63 horizontal 64 ) 65 66 type BSShip struct { 67 name string 68 x, y, size int 69 direction Direction 70 reversed bool 71 health int 72 } 73 74 func newShip(name string, x, y, size int, dir Direction, reversed bool) BSShip { 75 return BSShip{name: name, x: x, y: y, size: size, direction: dir, health: size, reversed: reversed} 76 } 77 78 func (s BSShip) contains(pos int) bool { 79 for _, el := range s.getPos() { 80 if pos == el { 81 return true 82 } 83 } 84 return false 85 } 86 87 func (s BSShip) getPos() (out []int) { 88 for i := 0; i < s.size; i++ { 89 incr := i 90 if s.direction == vertical { 91 incr *= 10 92 } 93 out = append(out, s.y*10+s.x+incr) 94 } 95 return 96 } 97 98 type BSCard struct { 99 carrier BSShip 100 battleShip BSShip 101 cruiser BSShip 102 submarine BSShip 103 destroyer BSShip 104 } 105 106 func (c *BSCard) collide(newShip BSShip) bool { 107 for _, p := range newShip.getPos() { 108 if c.carrier.contains(p) || 109 c.battleShip.contains(p) || 110 c.cruiser.contains(p) || 111 c.submarine.contains(p) || 112 c.destroyer.contains(p) { 113 return true 114 } 115 } 116 return false 117 } 118 119 func (c *BSCard) shot(pos int) { 120 if c.carrier.contains(pos) { 121 c.carrier.health -= 1 122 } else if c.battleShip.contains(pos) { 123 c.battleShip.health -= 1 124 } else if c.cruiser.contains(pos) { 125 c.cruiser.health -= 1 126 } else if c.submarine.contains(pos) { 127 c.submarine.health -= 1 128 } else if c.destroyer.contains(pos) { 129 c.destroyer.health -= 1 130 } 131 } 132 133 func (c BSCard) allShipsDead() bool { 134 return c.carrier.health == 0 && 135 c.battleShip.health == 0 && 136 c.cruiser.health == 0 && 137 c.submarine.health == 0 && 138 c.destroyer.health == 0 139 } 140 141 func (c BSCard) shipAt(pos int) (string, bool) { 142 if c.carrier.contains(pos) { 143 return "carrier", c.carrier.health == 0 144 } else if c.battleShip.contains(pos) { 145 return "battleShip", c.battleShip.health == 0 146 } else if c.cruiser.contains(pos) { 147 return "cruiser", c.cruiser.health == 0 148 } else if c.submarine.contains(pos) { 149 return "submarine", c.submarine.health == 0 150 } else if c.destroyer.contains(pos) { 151 return "destroyer", c.destroyer.health == 0 152 } 153 return "", false 154 } 155 156 func (c BSCard) hasShipAt(pos int) bool { 157 var allPos []int 158 allPos = append(allPos, c.carrier.getPos()...) 159 allPos = append(allPos, c.battleShip.getPos()...) 160 allPos = append(allPos, c.cruiser.getPos()...) 161 allPos = append(allPos, c.submarine.getPos()...) 162 allPos = append(allPos, c.destroyer.getPos()...) 163 for _, el := range allPos { 164 if pos == el { 165 return true 166 } 167 } 168 return false 169 } 170 171 type BSGame struct { 172 lastUpdated time.Time 173 turn int 174 player1 *BSPlayer 175 player2 *BSPlayer 176 } 177 178 func newGame(player1, player2 database.User) *BSGame { 179 g := new(BSGame) 180 g.lastUpdated = time.Now() 181 g.player1 = newPlayer(player1) 182 g.player2 = newPlayer(player2) 183 return g 184 } 185 186 func (g BSGame) IsPlayerTurn(playerID database.UserID) bool { 187 return g.turn == 0 && g.player1.id == playerID || 188 g.turn == 1 && g.player2.id == playerID 189 } 190 191 func (g *BSGame) Shot(pos string) (shipStr string, shipDead, gameEnded bool, err error) { 192 g.lastUpdated = time.Now() 193 rowStr := pos[0] 194 row := int(rowStr - 'A') 195 col, _ := strconv.Atoi(string(pos[1])) 196 p := row*10 + col 197 198 ent1 := g.player1 199 ent2 := g.player2 200 if g.turn == 1 { 201 ent1, ent2 = ent2, ent1 202 } 203 if _, ok := ent1.shots[p]; ok { 204 return "", false, false, errors.New("position already hit") 205 } 206 ent1.shots[p] = struct{}{} 207 ent2.card.shot(p) 208 shipStr, shipDead = ent2.card.shipAt(p) 209 gameEnded = ent2.card.allShipsDead() 210 211 g.turn = (g.turn + 1) % 2 212 return 213 } 214 215 type Battleship struct { 216 sync.Mutex 217 db *database.DkfDB 218 zeroID database.UserID 219 games map[string]*BSGame 220 } 221 222 func NewBattleship(db *database.DkfDB) *Battleship { 223 zeroUser := dutils.GetZeroUser(db) 224 b := &Battleship{db: db, zeroID: zeroUser.ID} 225 b.games = make(map[string]*BSGame) 226 227 // Thread that cleanup inactive games 228 go func() { 229 for { 230 time.Sleep(time.Minute) 231 b.Lock() 232 for k, g := range b.games { 233 if time.Since(g.lastUpdated) > 5*time.Minute { 234 delete(b.games, k) 235 } 236 } 237 b.Unlock() 238 } 239 }() 240 241 return b 242 } 243 244 func generateCard() *BSCard { 245 c := new(BSCard) 246 genTmpShip := func(name string, size int) (out BSShip) { 247 reversed := utils.RandBool() 248 dir := utils.RandChoice([]Direction{horizontal, vertical}) 249 val1 := utils.RandInt(0, 9) 250 val2 := utils.RandInt(0, 9-size) 251 if dir == horizontal { 252 val1, val2 = val2, val1 253 } 254 out = newShip(name, val1, val2, size, dir, reversed) 255 return 256 } 257 for _, i := range []int{0, 1, 2, 3, 4} { // iterate 5 times (for each boat) 258 names := []string{"carrier", "battleship", "cruiser", "submarine", "destroyer"} 259 sizes := []int{5, 4, 3, 3, 2} // respective boat size 260 for { 261 tmpShip := genTmpShip(names[i], sizes[i]) 262 // If boat collide with another boat, we need to generate a new position for that boat 263 if c.collide(tmpShip) { 264 continue 265 } 266 // boat position is valid, assign it 267 switch i { 268 case 0: 269 c.carrier = tmpShip 270 case 1: 271 c.battleShip = tmpShip 272 case 2: 273 c.cruiser = tmpShip 274 case 3: 275 c.submarine = tmpShip 276 case 4: 277 c.destroyer = tmpShip 278 } 279 break 280 } 281 } 282 return c 283 } 284 285 func (g *BSGame) drawCardFor(tmp int, roomName string, isNewGame, shipDead, gameEnded bool, shipStr, pos string) (out string) { 286 you := g.player1 287 enemy := g.player2 288 if tmp == 1 { 289 you = g.player2 290 enemy = g.player1 291 } 292 293 imgB64Fn := func(myCard bool) string { 294 ent1 := enemy 295 ent2 := you 296 if myCard { 297 ent1 = you 298 ent2 = enemy 299 } 300 301 c := gg.NewContext(177, 177) 302 303 c.Push() 304 c.SetColor(color.White) 305 c.DrawRectangle(0, 0, 177, 177) 306 c.Fill() 307 c.Pop() 308 309 c.Push() 310 c.SetColor(color.Black) 311 x := 22.0 312 y := 13.0 313 c.DrawString("0", x, y) 314 c.DrawString("1", x+16, y) 315 c.DrawString("2", x+16+16, y) 316 c.DrawString("3", x+16+16+16, y) 317 c.DrawString("4", x+16+16+16+16, y) 318 c.DrawString("5", x+16+16+16+16+16, y) 319 c.DrawString("6", x+16+16+16+16+16+16, y) 320 c.DrawString("7", x+16+16+16+16+16+16+16, y) 321 c.DrawString("8", x+16+16+16+16+16+16+16+16, y) 322 c.DrawString("9", x+16+16+16+16+16+16+16+16+16, y) 323 x = 6 324 y = 29.0 325 c.DrawString("A", x, y) 326 c.DrawString("B", x, y+16) 327 c.DrawString("C", x, y+16+16) 328 c.DrawString("D", x, y+16+16+16) 329 c.DrawString("E", x, y+16+16+16+16) 330 c.DrawString("F", x, y+16+16+16+16+16) 331 c.DrawString("G", x, y+16+16+16+16+16+16) 332 c.DrawString("H", x, y+16+16+16+16+16+16+16) 333 c.DrawString("I", x, y+16+16+16+16+16+16+16+16) 334 c.DrawString("J", x, y+16+16+16+16+16+16+16+16+16) 335 c.Pop() 336 337 c.Push() 338 c.SetLineWidth(1) 339 c.SetColor(color.RGBA{R: 90, G: 90, B: 90, A: 255}) 340 for col := 0.0; col < 12; col++ { 341 c.MoveTo(0.5+col*16, 0) 342 c.LineTo(0.5+col*16, 176) 343 c.Stroke() 344 } 345 for row := 0.0; row < 12; row++ { 346 c.MoveTo(0, 0.5+row*16) 347 c.LineTo(176, 0.5+row*16) 348 c.Stroke() 349 } 350 c.Pop() 351 352 drawShip := func(s BSShip) { 353 if !myCard && s.health != 0 && !gameEnded { 354 return 355 } 356 //fmt.Println(s.name, s.x, s.y, s.direction, s.reversed) 357 c.Push() 358 c.Translate(0.5, 0.5) 359 c.Translate(16, 16) 360 c.Translate(float64(s.x)*16, float64(s.y)*16) 361 if s.direction == horizontal { 362 if s.reversed { 363 c.Translate(float64(s.size)*16, 0) 364 c.Rotate(gg.Radians(90)) 365 } else { 366 c.Translate(0, 16) 367 c.Rotate(gg.Radians(-90)) 368 } 369 } else { 370 if s.reversed { 371 c.Translate(16, float64(s.size)*16) 372 c.Rotate(gg.Radians(180)) 373 } 374 } 375 // Front of the ship 376 c.MoveTo(1, 11) 377 c.QuadraticTo(8, -10, 15, 11) 378 // Length of the ship 379 c.Translate(0, float64(s.size-1)*16) 380 // back of the ship 381 c.LineTo(15, 11) 382 c.QuadraticTo(8, 17, 1, 11) 383 c.ClosePath() 384 if s.health == 0 { 385 c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 200}) 386 c.Fill() 387 } else if !myCard && gameEnded { 388 c.SetColor(color.RGBA{R: 100, G: 130, B: 100, A: 200}) 389 c.Fill() 390 } else { 391 c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 255}) 392 c.Fill() 393 } 394 c.Pop() 395 } 396 drawShip(ent1.card.carrier) 397 drawShip(ent1.card.battleShip) 398 drawShip(ent1.card.cruiser) 399 drawShip(ent1.card.submarine) 400 drawShip(ent1.card.destroyer) 401 402 c.Push() 403 c.Translate(0.5, 0.5) 404 c.Translate(16, 16) 405 for shot := range ent2.shots { 406 shotRow := shot / 10 407 shotCol := shot % 10 408 c.Push() 409 c.Translate(float64(shotCol)*16, float64(shotRow)*16) 410 411 if ent1.card.hasShipAt(shot) { 412 c.DrawCircle(8, 8, 4) 413 c.SetColor(color.RGBA{R: 255, G: 200, B: 0, A: 255}) 414 c.Fill() 415 416 c.DrawCircle(8, 8, 3) 417 c.SetColor(color.RGBA{R: 255, G: 0, B: 0, A: 255}) 418 c.Fill() 419 } else { 420 c.DrawCircle(8, 8, 3) 421 c.SetColor(color.RGBA{R: 60, G: 200, B: 0, A: 255}) 422 c.Fill() 423 424 c.DrawCircle(8, 8, 2) 425 c.SetColor(color.RGBA{R: 100, G: 0, B: 0, A: 255}) 426 c.Fill() 427 } 428 429 c.Pop() 430 } 431 c.Pop() 432 433 var buf bytes.Buffer 434 _ = c.EncodePNG(&buf) 435 imgB64 := base64.StdEncoding.EncodeToString(buf.Bytes()) 436 return imgB64 437 } 438 439 imgB64 := imgB64Fn(true) 440 img1B64 := imgB64Fn(false) 441 442 htmlTmpl := ` 443 Against <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span><br /> 444 {{ if not .IsNewGame }} 445 {{ if .YourTurn }} 446 <span {{ .EnemyUserStyle | HTMLAttr }}>@{{ .EnemyUsername }}</span> played {{ .Pos }} 447 {{ else }} 448 you played {{ .Pos }} 449 {{ end }} 450 ; 451 {{ if .ShipStr }} 452 {{ .ShipStr }} hit 453 {{ if .ShipDead }} 454 and sunk 455 {{ end }} 456 {{ else }} 457 miss 458 {{ end }} 459 ; 460 {{ end }} 461 {{ if .GameEnded }} 462 {{ if .YourTurn }} 463 You lost!<br /> 464 {{ else }} 465 You win!<br /> 466 {{ end }} 467 {{ else }} 468 {{ if .YourTurn }} 469 now is your turn<br /> 470 {{ else }} 471 waiting for opponent<br /> 472 {{ end }} 473 {{ end }} 474 <table> 475 <tr> 476 <td><img src="data:image/png;base64,{{ .ImgB64 }}" alt="" /></td> 477 <td style="vertical-align: top;"> 478 <form method="post" style="margin-left: 10px;" action="/api/v1/battleship"> 479 <input type="hidden" name="room" value="{{ .RoomName }}" /> 480 <input type="hidden" name="enemyUsername" value="{{ .EnemyUsername }}" /> 481 <table style="width: 177px; height: 177px; background-image: url(data:image/png;base64,{{ .Img1B64 }})"> 482 <tr style="height: 16px;"><td colspan="11"> </td></tr> 483 {{- range $row := .Rows -}} 484 <tr style="height: 16px;"> 485 <td style="width: 16px;"></td> 486 {{- range $col := $.Cols -}} 487 {{- if NotShot $row $col -}} 488 {{- if and $.YourTurn (not $.GameEnded) -}} 489 <td style="width: 16px;"> 490 <button style="height: 15px; width: 15px;" name="move" value="{{ GetRune $row }}{{ $col }}"></button> 491 </td> 492 {{- else -}} 493 <td style="width: 16px;"></td> 494 {{- end -}} 495 {{- else -}} 496 <td style="width: 16px;"></td> 497 {{- end -}} 498 {{- end -}} 499 </tr> 500 {{- end -}} 501 </table> 502 </form> 503 </td> 504 </tr> 505 </table> 506 ` 507 data := map[string]any{ 508 "RoomName": roomName, 509 "EnemyUserStyle": enemy.userStyle, 510 "EnemyUsername": enemy.username, 511 "IsNewGame": isNewGame, 512 "YourTurn": g.turn == tmp, 513 "Pos": pos, 514 "ShipStr": shipStr, 515 "ShipDead": shipDead, 516 "GameEnded": gameEnded, 517 "ImgB64": imgB64, 518 "Img1B64": img1B64, 519 "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 520 "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 521 } 522 fns := template.FuncMap{ 523 "GetRune": func(i int) string { 524 return string(rune('A' + i)) 525 }, 526 "NotShot": func(i, j int) bool { 527 _, ok := you.shots[i*10+j] 528 return !ok 529 }, 530 "HTMLAttr": func(in string) template.HTMLAttr { 531 return template.HTMLAttr(in) 532 }, 533 } 534 var buf bytes.Buffer 535 _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data) 536 return buf.String() 537 } 538 539 func (b *Battleship) InterceptMsg(cmd *command.Command) { 540 m := bsRgx.FindStringSubmatch(cmd.Message) 541 if len(m) != 3 { 542 return 543 } 544 enemyUsername := database.Username(m[1]) 545 pos := m[2] 546 if err := b.PlayMove(cmd.Room.Name, cmd.Room.ID, cmd.RoomKey, *cmd.AuthUser, enemyUsername, pos); err != nil { 547 cmd.Err = err 548 return 549 } 550 cmd.Err = command.ErrStop 551 return 552 } 553 554 func (b *Battleship) PlayMove(roomName string, roomID database.RoomID, roomKey string, authUser database.User, enemyUsername database.Username, pos string) error { 555 b.Lock() 556 defer b.Unlock() 557 558 user, err := b.db.GetUserByUsername(enemyUsername) 559 if err != nil { 560 return errors.New("invalid username") 561 } 562 563 var gameKey string 564 if authUser.ID < user.ID { 565 gameKey = fmt.Sprintf("%d_%d", authUser.ID, user.ID) 566 } else { 567 gameKey = fmt.Sprintf("%d_%d", user.ID, authUser.ID) 568 } 569 570 var shipStr string 571 var isNewGame, shipDead, gameEnded bool 572 g, ok := b.games[gameKey] 573 if ok { 574 if !g.IsPlayerTurn(authUser.ID) { 575 return errors.New("not your turn") 576 } 577 shipStr, shipDead, gameEnded, err = g.Shot(pos) 578 if err != nil { 579 return err 580 } 581 } else { 582 if pos != "" { 583 return errors.New("no Game ongoing") 584 } 585 g = newGame(user, authUser) 586 b.games[gameKey] = g 587 isNewGame = true 588 } 589 590 // Delete old messages sent by "0" to the players 591 if err := b.db.DB(). 592 Where("room_id = ? AND user_id = ? AND (to_user_id = ? OR to_user_id = ?)", roomID, b.zeroID, g.player1.id, g.player2.id). 593 Delete(&database.ChatMessage{}).Error; err != nil { 594 logrus.Error(err) 595 } 596 597 card1 := g.drawCardFor(0, roomName, isNewGame, shipDead, gameEnded, shipStr, pos) 598 _, _ = b.db.CreateMsg(card1, card1, roomKey, roomID, b.zeroID, &g.player1.id, false) 599 600 card2 := g.drawCardFor(1, roomName, isNewGame, shipDead, gameEnded, shipStr, pos) 601 _, _ = b.db.CreateMsg(card2, card2, roomKey, roomID, b.zeroID, &g.player2.id, false) 602 603 if gameEnded { 604 delete(b.games, gameKey) 605 } 606 607 return nil 608 }