From 724fdd3014edef7b5667d9271e76d6096796ef74 Mon Sep 17 00:00:00 2001 From: Fredrik Robertsen Date: Sat, 6 Jun 2026 20:21:22 +0200 Subject: [PATCH] almost finish action handling --- src/game.odin | 8 ++- src/server.odin | 148 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/game.odin b/src/game.odin index b428081..603fc4e 100644 --- a/src/game.odin +++ b/src/game.odin @@ -19,7 +19,7 @@ Card :: bit_field u8 { _player: u8 | 2, } -Hand :: distinct []Card +Hand :: distinct [dynamic]Card // 6 piles, one for each in COLORS. // u8 between 0-5, representing highest card in pile. @@ -37,6 +37,7 @@ Belief_State :: []bit_field u16 { Game :: struct { num_players: int, + max_hints: int, num_hints: int, num_lives: int, hand_size: int, @@ -44,6 +45,7 @@ Game :: struct { player_hands: []Hand, player_beliefs: []Belief_State, deck: [dynamic]Card, + discarded: [dynamic]Card, } create_deck :: proc() -> (deck: [dynamic]Card) { @@ -64,7 +66,8 @@ deal_hands :: proc( player_hands: [dynamic]Hand for i in 0 ..< num_players { start := len(deck) - hand_size - hand := Hand(deck[start:]) + hand: Hand + for c in deck[start:] do append(&hand, c) append(&player_hands, hand) resize(deck, start) } @@ -90,6 +93,7 @@ init_beliefs :: proc( create_game :: proc(hint_tokens, lives_left, num_players: int) -> (s: Game) { assert(num_players >= 2 && num_players <= 5) s.num_players = num_players + s.max_hints = hint_tokens s.num_hints = hint_tokens s.num_lives = lives_left s.hand_size = num_players <= 3 ? 5 : 4 diff --git a/src/server.odin b/src/server.odin index 10a9d8a..3b72c73 100644 --- a/src/server.odin +++ b/src/server.odin @@ -4,6 +4,8 @@ import "core:fmt" import "core:math/rand" import "core:mem" import "core:net" +import "core:strconv" +import "core:strings" run_server :: proc(listen_addr: net.Endpoint, num_players: int, num_hints: int, num_lives: int) { listener, list_err := net.listen_tcp(listen_addr) @@ -23,8 +25,10 @@ run_server :: proc(listen_addr: net.Endpoint, num_players: int, num_hints: int, current_player := rand.int_max(num_players) GAME_LOOP: for { + views := make([dynamic]Player_View, context.temp_allocator) for i in 0 ..< num_players { view := create_view_from_game(g, i, context.temp_allocator) + append(&views, view) send_err := send_payload(comm[i], {.State_Update, view}) if send_err != nil do panic(fmt.tprintln("failed to send payload:", send_err)) } @@ -39,7 +43,7 @@ run_server :: proc(listen_addr: net.Endpoint, num_players: int, num_hints: int, } action := receive_action(comm[current_player]) - + translate_action(action, current_player) perform_action(&g, action) current_player = (current_player + 1) % num_players free_all(context.temp_allocator) @@ -129,7 +133,7 @@ Info_Data :: distinct []byte Payload :: struct #packed { type: Msg_Type, - body: union { + body: union #no_nil { Player_View, Info_Data, }, @@ -149,10 +153,6 @@ send_payload :: proc(sock: net.TCP_Socket, pl: Payload) -> net.Network_Error { } send_state_update :: proc(sock: net.TCP_Socket, view: ^Player_View) -> net.Network_Error { - // header := Msg_Type.State_Update - // net.send(sock, mem.ptr_to_bytes(&header)) or_return - // net.send(sock, mem.ptr_to_bytes(pl)) or_return - // return nil type := Msg_Type.State_Update net.send(sock, mem.ptr_to_bytes(&type)) or_return // everything up to `cards` field ("header") @@ -188,13 +188,13 @@ send_info :: proc(sock: net.TCP_Socket, data: Info_Data) -> net.Network_Error { // and the global player index used by g.player_hands. // thus, most of this section should likely be moved into game.odin -Play_Action :: distinct Card -Discard_Action :: distinct Card +Play_Action :: distinct int +Discard_Action :: distinct int Value_Hint :: distinct int -Color_Hint :: distinct int +Color_Hint :: distinct COLORS Hint_Action :: struct { target_player: int, // global index - type: union { + type: union #no_nil { Value_Hint, Color_Hint, }, @@ -216,15 +216,14 @@ where color is one of - w[hite] - b[lue] - y[ellow] - - r[ainbow] + - [x|rainbow] and value is one of 1, 2, 3, 4, 5. -whitespace does not matter, so you could write shorthands like "play1" or "d3". - example: $ hint 2 red $ play 5 - $ hint 3 5` + $ hint 3 5 + $ h 1 b` receive_action :: proc(player: net.TCP_Socket) -> (action: Action) { POKE: for { @@ -242,10 +241,123 @@ receive_action :: proc(player: net.TCP_Socket) -> (action: Action) { return } -parse_action :: proc(raw_action: string) -> Action { - unimplemented() +parse_action :: proc(raw_action: string) -> (action: Action) { + command := strings.split(raw_action, " ") + if len(command) < 1 do return nil + switch command[0] { + case "play", "p": + if len(command) - 1 < 1 do return nil + card_id, ok := strconv.parse_int(command[1]) + if !ok || card_id < 1 || card_id > 5 do return nil + action = Play_Action(card_id) + case "discard", "d": + if len(command) - 1 < 1 do return nil + card_id, ok := strconv.parse_int(command[1]) + if !ok || card_id < 1 || card_id > 5 do return nil + action = Discard_Action(card_id) + case "hint", "h": + if len(command) - 1 < 2 do return nil + player_id, pok := strconv.parse_int(command[1]) + if !pok || player_id < 1 || player_id > 4 do return nil + hint: Hint_Action + hint.target_player = player_id + switch command[2] { + case "red", "r": + hint.type = .RED + case "green", "g": + hint.type = .GREEN + case "white", "w": + hint.type = .WHITE + case "blue", "b": + hint.type = .BLUE + case "yellow", "y": + hint.type = .YELLOW + case "rainbow", "x": + hint.type = .RAINBOW + case: + value, vok := strconv.parse_int(command[2]) + if !vok || value < 1 || value > 5 do return nil + hint.type = Value_Hint(value) + } + action = hint + } + return } -perform_action :: proc(game: ^Game, action: Action) { - unimplemented() +// the action produced by `parse_action` is the raw action provided by the player, +// but the player uses their own local ids to reference cards and players. +// this function modifies an action with local ids to use the global indices kept +// track of by the server. this translation happens in-place and is necessary to +// do before performing the action with `perform_action`. +// more specifically, this is what happens: +// - play/discard decrement their value to reflect the index of the card, +// i.e. card id 5 -> 4, such that game.player_hands[current_player][id] +// yields the relevant card. thus the local indices for the player +// should *always* reflect the underlying indices into the player hand. +// - hint actions need to resolve which target player is the correct global one. +// since we may have players with indices 0-4, this would necessitate the use of +// 3 bits, but each card only has a 2-bit field _player for indexing purposes. +// this field thus references the player's local view, so that each player sees +// players 0-3. this local value encoded in the hint action thus far needs to +// be converted to its global value. +translate_action :: proc(action: ^Action, current_player: int) { + switch &a in action { + case Play_Action: + a -= 1 + assert(a >= 0 && a <= 4) // for some player-numbers, a == 4 should be disallowed + case Discard_Action: + a -= 1 + assert(a >= 0 && a <= 4) // --||-- + case Hint_Action: + if current_player > a.target_player { + a.target_player -= 1 + } else { + a.target_player += 1 + } + a.target_player -= 1 + assert(a.target_player >= 0 && a.target_player <= 4) + } +} + +// // 6 piles, one for each in COLORS. +// // u8 between 0-5, representing highest card in pile. +// // player can only place card of value `played[color]+1`. +// Played_Card_Piles :: distinct [6]u8 + + +// assumes action is translated. see `translate_action`. +perform_action :: proc(game: ^Game, action: Action, current_player: int) { + switch a in action { + case Play_Action: + hand := &game.player_hands[current_player] + card := hand[a] + ordered_remove(hand, a) + if card.value == game.played[card.color] + 1 { + game.played[card.color] += 1 + } else { + append(&game.discarded, card) + } + new_card := pop(&game.deck) + append(hand, new_card) + case Discard_Action: + hand := &game.player_hands[current_player] + card := hand[a] + ordered_remove(hand, a) + append(&game.discarded, card) + new_card := pop(&game.deck) + append(hand, new_card) + if game.num_hints < game.max_hints do game.num_hints += 1 + case Hint_Action: + belief := game.player_beliefs[a.target_player] + switch hint in a.type { + case Value_Hint: + // TODO: restructure Game to couple Card and Belief, since the belief is per card, + // having them in separate arrays where indices get messy is a bad idea. they + // should be unified in a single super Card. + // maybe change Card to Card_Data, which is then the data that will be sent in the + // view, and then the new Card will contain all other server-side metadata such as + // belief states and global player ids. + case Color_Hint: + } + } }