implement game turn logic
This commit is contained in:
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user