implement game turn logic

This commit is contained in:
2026-11-02 21:24:32 +03:00
parent 153dc74ff6
commit 189b027a11
7 changed files with 896 additions and 13 deletions
+54 -1
View File
@@ -12,8 +12,10 @@
// 3 S->C STATE [3, <seq>, <state_code>]
// 4 C<->S ACK [4, <seq>]
// 5 C->S PLACE_SHIPS [5, ship0_orientation, ship0_x, ship0_y, ...]
// 6 C->S FIRE [6, <x>, <y>]
// 7 S->C FIRE_RESULT [7, <seq>, <result_code>]
//
// Every state-changing server->client message (END, STATE) carries a
// Every state-changing server->client message (END, STATE, FIRE_RESULT) carries a
// per-player monotonically increasing sequence number. The client confirms
// delivery with an ACK message containing the sequence number of the last
// received message (ACK is cumulative). Unacknowledged messages are kept in
@@ -36,6 +38,8 @@ const (
OpState = 3 // S->C: game state transition
OpAck = 4 // C<->S: delivery acknowledgement
OpPlaceShips = 5 // C->S: ship placement
OpFire = 6 // C->S: fire shot
OpFireResult = 7 // S->C: shot result
)
// Board and ship configuration constants.
@@ -89,9 +93,19 @@ const (
StateWaitOpponentTurn = 5 // wait for the opponent's move
)
// Shot result codes for FIRE_RESULT (OpFireResult).
const (
FireResultMiss = 1 // miss (промах)
FireResultHit = 2 // hit (простое попадание)
FireResultSunk = 3 // ship sunk (корабль потоплен)
FireResultInvalidPosition = 4 // invalid position (неверная позиция)
)
// Match end codes.
const (
EndCodeJoinTimeout = -1 // the second player never joined
EndCodeVictory = 1 // you won (all opponent ships sunk)
EndCodeDefeat = 2 // you lost (all your ships sunk)
)
// WebSocket close codes used by the server (application range 4000+ and RFC 6455 codes).
@@ -275,3 +289,42 @@ func BuildPlaceShips(ships []Ship) []byte {
}
return build(v)
}
// ParseFire extracts the x and y coordinates from FIRE arguments.
func ParseFire(args []json.RawMessage) (x, y int, err error) {
if len(args) != 2 {
return 0, 0, fmt.Errorf("FIRE expects 2 arguments, got %d", len(args))
}
if err := json.Unmarshal(args[0], &x); err != nil {
return 0, 0, fmt.Errorf("x coordinate is not an integer: %w", err)
}
if err := json.Unmarshal(args[1], &y); err != nil {
return 0, 0, fmt.Errorf("y coordinate is not an integer: %w", err)
}
return x, y, nil
}
// BuildFire builds a FIRE message: [6, x, y].
func BuildFire(x, y int) []byte {
return build([]any{OpFire, x, y})
}
// ParseFireResult extracts seq and result code from FIRE_RESULT arguments.
func ParseFireResult(args []json.RawMessage) (seq int64, result int, err error) {
if len(args) != 2 {
return 0, 0, fmt.Errorf("FIRE_RESULT expects 2 arguments, got %d", len(args))
}
if err := json.Unmarshal(args[0], &seq); err != nil {
return 0, 0, fmt.Errorf("seq is not a number: %w", err)
}
if err := json.Unmarshal(args[1], &result); err != nil {
return 0, 0, fmt.Errorf("result is not a number: %w", err)
}
return seq, result, nil
}
// BuildFireResult builds a FIRE_RESULT message: [7, seq, result].
func BuildFireResult(seq int64, result int) []byte {
return build([]any{OpFireResult, seq, result})
}
@@ -262,3 +262,90 @@ func TestValidateShips(t *testing.T) {
}
})
}
func TestFireRoundtrip(t *testing.T) {
data := BuildFire(3, 7)
opcode, args, err := ParseMessage(data)
if err != nil {
t.Fatalf("ParseMessage: %v", err)
}
if opcode != OpFire {
t.Fatalf("opcode = %d, want %d", opcode, OpFire)
}
x, y, err := ParseFire(args)
if err != nil {
t.Fatalf("ParseFire: %v", err)
}
if x != 3 || y != 7 {
t.Fatalf("got (%d, %d), want (3, 7)", x, y)
}
}
func TestParseFireErrors(t *testing.T) {
t.Run("wrong argument count", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[6, 1]`))
if _, _, err := ParseFire(args); err == nil {
t.Fatal("expected error for 1 argument")
}
_, args3, _ := ParseMessage([]byte(`[6, 1, 2, 3]`))
if _, _, err := ParseFire(args3); err == nil {
t.Fatal("expected error for 3 arguments")
}
})
t.Run("x not integer", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[6, "a", 2]`))
if _, _, err := ParseFire(args); err == nil {
t.Fatal("expected error for string x")
}
})
t.Run("y not integer", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[6, 1, true]`))
if _, _, err := ParseFire(args); err == nil {
t.Fatal("expected error for bool y")
}
})
}
func TestFireResultRoundtrip(t *testing.T) {
data := BuildFireResult(5, FireResultSunk)
opcode, args, err := ParseMessage(data)
if err != nil {
t.Fatalf("ParseMessage: %v", err)
}
if opcode != OpFireResult {
t.Fatalf("opcode = %d, want %d", opcode, OpFireResult)
}
seq, result, err := ParseFireResult(args)
if err != nil {
t.Fatalf("ParseFireResult: %v", err)
}
if seq != 5 || result != FireResultSunk {
t.Fatalf("got seq=%d, result=%d, want seq=5, result=%d", seq, result, FireResultSunk)
}
}
func TestParseFireResultErrors(t *testing.T) {
t.Run("wrong argument count", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[7, 1]`))
if _, _, err := ParseFireResult(args); err == nil {
t.Fatal("expected error for 1 argument")
}
})
t.Run("seq not int64", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[7, "str", 1]`))
if _, _, err := ParseFireResult(args); err == nil {
t.Fatal("expected error for string seq")
}
})
t.Run("result not int", func(t *testing.T) {
_, args, _ := ParseMessage([]byte(`[7, 1, "hit"]`))
if _, _, err := ParseFireResult(args); err == nil {
t.Fatal("expected error for string result")
}
})
}