From 0fc8ccc50e44803aff397dc7ce91f7eb64879639 Mon Sep 17 00:00:00 2001 From: Kashargul <144968721+Kashargul@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:02:32 +0100 Subject: [PATCH] game collection (#19179) * game collection * no phased usage * . * spellcheck * chesskers * fixes runtime during del --------- Co-authored-by: Cameron Lennox --- code/__defines/board_game.dm | 10 + .../modules/casino/board_games/board_table.dm | 87 +++ code/modules/casino/board_games/checkers.dm | 523 ++++++++++++++++ code/modules/casino/board_games/chess.dm | 566 ++++++++++++++++++ .../casino/board_games/four_in_a_row.dm | 330 ++++++++++ code/modules/casino/board_games/nine_mens.dm | 421 +++++++++++++ code/modules/casino/board_games/rpg_dice.dm | 72 +++ .../casino/board_games/space_battle.dm | 436 ++++++++++++++ .../modules/casino/board_games/tic_tac_toe.dm | 51 ++ .../casino/board_games/vore_sweeper.dm | 321 ++++++++++ code/modules/mob/mob_helpers.dm | 23 +- code/modules/pda/app.dm | 6 +- code/modules/pda/cart_apps.dm | 16 +- code/modules/pda/cart_vr.dm | 6 +- code/modules/pda/core_apps.dm | 10 +- code/modules/pda/game_launcher.dm | 125 ++++ code/modules/pda/messenger.dm | 2 +- code/modules/pda/messenger_plugins.dm | 10 +- code/modules/pda/nerdle.dm | 2 +- code/modules/pda/pda.dm | 11 +- code/modules/pda/pda_subtypes.dm | 2 +- code/modules/pda/utilities.dm | 14 +- code/modules/tgui/states/board_game.dm | 26 + icons/obj/casino.dmi | Bin 80610 -> 81735 bytes icons/obj/tables.dmi | Bin 33247 -> 31925 bytes .../ChessCheckers/CommonComponents.tsx | 18 + .../interfaces/ChessCheckers/GameArea.tsx | 150 +++++ .../interfaces/ChessCheckers/PlayerMenu.tsx | 126 ++++ .../interfaces/ChessCheckers/PlayerPanel.tsx | 50 ++ .../ChessCheckers/PromotionSelection.tsx | 48 ++ .../interfaces/ChessCheckers/constants.ts | 32 + .../interfaces/ChessCheckers/functions.ts | 74 +++ .../tgui/interfaces/ChessCheckers/index.tsx | 21 + .../tgui/interfaces/ChessCheckers/types.ts | 17 + .../tgui/interfaces/FourInARow/GameArea.tsx | 122 ++++ .../tgui/interfaces/FourInARow/PlayerMenu.tsx | 177 ++++++ .../interfaces/FourInARow/PlayerPanel.tsx | 67 +++ .../tgui/interfaces/FourInARow/constants.ts | 5 + .../tgui/interfaces/FourInARow/functions.ts | 39 ++ .../tgui/interfaces/FourInARow/index.tsx | 27 + .../tgui/interfaces/FourInARow/types.ts | 18 + .../tgui/interfaces/NineMen/GameArea.tsx | 81 +++ .../tgui/interfaces/NineMen/PlayerMenu.tsx | 125 ++++ .../tgui/interfaces/NineMen/PlayerPanel.tsx | 44 ++ .../tgui/interfaces/NineMen/constants.ts | 42 ++ .../tgui/interfaces/NineMen/functions.ts | 38 ++ .../tgui/interfaces/NineMen/index.tsx | 21 + .../packages/tgui/interfaces/NineMen/types.ts | 18 + .../Pda/pda_screens/pda_game_launcher.tsx | 49 ++ .../interfaces/RpgDice.tsx/PlayerMenu.tsx | 248 ++++++++ .../tgui/interfaces/RpgDice.tsx/RollArea.tsx | 118 ++++ .../tgui/interfaces/RpgDice.tsx/constants.ts | 9 + .../tgui/interfaces/RpgDice.tsx/index.tsx | 21 + .../tgui/interfaces/RpgDice.tsx/types.ts | 15 + .../tgui/interfaces/SpaceBattle/GameArea.tsx | 277 +++++++++ .../interfaces/SpaceBattle/PlayerMenu.tsx | 166 +++++ .../interfaces/SpaceBattle/PlayerPanel.tsx | 96 +++ .../tgui/interfaces/SpaceBattle/constants.ts | 29 + .../tgui/interfaces/SpaceBattle/functions.ts | 40 ++ .../tgui/interfaces/SpaceBattle/index.tsx | 171 ++++++ .../tgui/interfaces/SpaceBattle/types.ts | 27 + .../tgui/interfaces/VoreSweeper/GameArea.tsx | 127 ++++ .../interfaces/VoreSweeper/PlayerMenu.tsx | 191 ++++++ .../tgui/interfaces/VoreSweeper/constants.ts | 25 + .../tgui/interfaces/VoreSweeper/functions.ts | 46 ++ .../tgui/interfaces/VoreSweeper/index.tsx | 31 + .../tgui/interfaces/VoreSweeper/types.ts | 13 + .../tgui/styles/interfaces/FourInARow.scss | 34 ++ .../tgui/styles/interfaces/NineMen.scss | 10 + .../tgui/styles/interfaces/RpgDice.scss | 107 ++++ tgui/packages/tgui/styles/main.scss | 3 + vorestation.dme | 12 + 72 files changed, 6248 insertions(+), 47 deletions(-) create mode 100644 code/__defines/board_game.dm create mode 100644 code/modules/casino/board_games/board_table.dm create mode 100644 code/modules/casino/board_games/checkers.dm create mode 100644 code/modules/casino/board_games/chess.dm create mode 100644 code/modules/casino/board_games/four_in_a_row.dm create mode 100644 code/modules/casino/board_games/nine_mens.dm create mode 100644 code/modules/casino/board_games/rpg_dice.dm create mode 100644 code/modules/casino/board_games/space_battle.dm create mode 100644 code/modules/casino/board_games/tic_tac_toe.dm create mode 100644 code/modules/casino/board_games/vore_sweeper.dm create mode 100644 code/modules/pda/game_launcher.dm create mode 100644 code/modules/tgui/states/board_game.dm create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/CommonComponents.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/GameArea.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/PlayerPanel.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/PromotionSelection.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/constants.ts create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/functions.ts create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/index.tsx create mode 100644 tgui/packages/tgui/interfaces/ChessCheckers/types.ts create mode 100644 tgui/packages/tgui/interfaces/FourInARow/GameArea.tsx create mode 100644 tgui/packages/tgui/interfaces/FourInARow/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/FourInARow/PlayerPanel.tsx create mode 100644 tgui/packages/tgui/interfaces/FourInARow/constants.ts create mode 100644 tgui/packages/tgui/interfaces/FourInARow/functions.ts create mode 100644 tgui/packages/tgui/interfaces/FourInARow/index.tsx create mode 100644 tgui/packages/tgui/interfaces/FourInARow/types.ts create mode 100644 tgui/packages/tgui/interfaces/NineMen/GameArea.tsx create mode 100644 tgui/packages/tgui/interfaces/NineMen/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/NineMen/PlayerPanel.tsx create mode 100644 tgui/packages/tgui/interfaces/NineMen/constants.ts create mode 100644 tgui/packages/tgui/interfaces/NineMen/functions.ts create mode 100644 tgui/packages/tgui/interfaces/NineMen/index.tsx create mode 100644 tgui/packages/tgui/interfaces/NineMen/types.ts create mode 100644 tgui/packages/tgui/interfaces/Pda/pda_screens/pda_game_launcher.tsx create mode 100644 tgui/packages/tgui/interfaces/RpgDice.tsx/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/RpgDice.tsx/RollArea.tsx create mode 100644 tgui/packages/tgui/interfaces/RpgDice.tsx/constants.ts create mode 100644 tgui/packages/tgui/interfaces/RpgDice.tsx/index.tsx create mode 100644 tgui/packages/tgui/interfaces/RpgDice.tsx/types.ts create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/GameArea.tsx create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/PlayerPanel.tsx create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/constants.ts create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/functions.ts create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/index.tsx create mode 100644 tgui/packages/tgui/interfaces/SpaceBattle/types.ts create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/GameArea.tsx create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/PlayerMenu.tsx create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/constants.ts create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/functions.ts create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/index.tsx create mode 100644 tgui/packages/tgui/interfaces/VoreSweeper/types.ts create mode 100644 tgui/packages/tgui/styles/interfaces/FourInARow.scss create mode 100644 tgui/packages/tgui/styles/interfaces/NineMen.scss create mode 100644 tgui/packages/tgui/styles/interfaces/RpgDice.scss diff --git a/code/__defines/board_game.dm b/code/__defines/board_game.dm new file mode 100644 index 0000000000..a5504a4b3a --- /dev/null +++ b/code/__defines/board_game.dm @@ -0,0 +1,10 @@ +#define GAME_SETUP 0 // Used across all datums in the same way, please keep further steps per game as they differ + +#define GAME_SWEEPER "VoreSweeper" +#define GAME_FOUR_ROW "Four in a row" +#define GAME_SPACE_BATTLE "SpaceBattle" +#define GAME_RGP_DICE "RPG dice" +#define GAME_CHESS "Chess" +#define GAME_CHECKERS "Checkers" +#define GAME_NINE_MENS_MORRIS "Nine men's morris" +#define GAME_TIC_TAC_TOE "Tic-Tac-Toe" diff --git a/code/modules/casino/board_games/board_table.dm b/code/modules/casino/board_games/board_table.dm new file mode 100644 index 0000000000..7849fbd6e9 --- /dev/null +++ b/code/modules/casino/board_games/board_table.dm @@ -0,0 +1,87 @@ +/obj/structure/casino_table/board_game + name = "board game table" + desc = "A collection of various board games." + icon_state = "gamble_preview" + var/datum/board_game/game_ui + var/static/list/possible_games = list( + GAME_SWEEPER = /datum/board_game/four_row, + GAME_FOUR_ROW = /datum/board_game/vore_sweeper, + GAME_SPACE_BATTLE = /datum/board_game/space_battle, + GAME_RGP_DICE = /datum/board_game/rpg_dice, + GAME_CHESS = /datum/board_game/chess, + GAME_CHECKERS = /datum/board_game/checkers, + GAME_NINE_MENS_MORRIS = /datum/board_game/nine_mens, + GAME_TIC_TAC_TOE = /datum/board_game/four_row/tic_tac_toe + ) + +/obj/structure/casino_table/board_game/Initialize(mapload) + . = ..() + if(ispath(game_ui)) + game_ui = new game_ui(src) + +/obj/structure/casino_table/board_game/Destroy() + if(game_ui) + game_ui.parent = null + QDEL_NULL(game_ui) + . = ..() + +/obj/structure/casino_table/board_game/attack_hand(mob/user) + . = ..() + if(isliving(user)) + if(!game_ui) + pick_game(user) + game_ui.tgui_interact(user) + +/obj/structure/casino_table/board_game/click_alt(mob/user) + pick_game(user) + +/obj/structure/casino_table/board_game/proc/pick_game(mob/user) + if(game_ui?.game_state != GAME_SETUP) + return + var/datum/board_game/new_game = tgui_input_list(user, "Pick the game to play", "Choose Game", possible_games) + if(!new_game) + return + new_game = possible_games[new_game] + if(game_ui) + QDEL_NULL(game_ui) + game_ui = new new_game(src) + icon_state = game_ui.table_icon + +/datum/board_game + var/name + var/atom/parent + var/game_state = GAME_SETUP + var/table_icon = "gamble_preview" + +/datum/board_game/tgui_state(mob/user) + return GLOB.tgui_board_game_state + +/datum/board_game/New(atom/holder) + . = ..() + parent = holder + +/datum/board_game/Destroy(force) + parent = null + . = ..() + +/datum/board_game/tgui_host(mob/user) + return parent + +/datum/board_game/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("invite_player") + var/list/possible_mobs = ui.user.living_mobs_in_view(1, TRUE, TRUE) + for(var/obj/belly/our_belly in ui.user.vore_organs) + for(var/mob/living/prey in our_belly.contents) + if(prey.client) + possible_mobs += prey + var/mob/living/new_player = tgui_input_list(ui.user, "Invite a nearby player to the game.", "Invite Player", possible_mobs) + if(!new_player) + return FALSE + tgui_interact(new_player) + return TRUE + return FALSE diff --git a/code/modules/casino/board_games/checkers.dm b/code/modules/casino/board_games/checkers.dm new file mode 100644 index 0000000000..5c9bdcd520 --- /dev/null +++ b/code/modules/casino/board_games/checkers.dm @@ -0,0 +1,523 @@ +#define GAME_PLAYER_ONE 1 +#define GAME_PLAYER_TWO 2 +#define GAME_OVER 3 +#define GAME_OVER_DRAW 4 +#define GRID_SIZE 8 + +#define GAME_ACTION_NONE 0 +#define GAME_ACTION_SELECT 1 +#define GAME_ACTION_END_TURN 2 + +/obj/structure/casino_table/board_game/checkers + name = GAME_CHECKERS + desc = "A classic checkers game." + icon_state = "gamble_chess" + game_ui = /datum/board_game/checkers + +/datum/board_game/checkers + name = GAME_CHECKERS + table_icon = "gamble_chess" + var/datum/weakref/player_one + var/datum/weakref/player_two + var/player_one_time = 0 + var/player_two_time = 0 + var/static/list/default_board = list( + list(null, "bM", null, "bM", null, "bM", null, "bM"), + list("bM", null, "bM", null, "bM", null, "bM", null), + list(null, "bM", null, "bM", null, "bM", null, "bM"), + list(null, null, null, null, null, null, null, null), + list(null, null, null, null, null, null, null, null), + list("wM", null, "wM", null, "wM", null, "wM", null), + list(null, "wM", null, "wM", null, "wM", null, "wM"), + list("wM", null, "wM", null, "wM", null, "wM", null) + ) + var/list/current_board = list() + var/list/valid_moves = list() + var/list/selected_figure = list() + var/list/possible_jumps = list() + var/turn_start_time = 0 + var/winner + +/datum/board_game/checkers/Destroy(force) + player_one = null + player_two = null + . = ..() + +/datum/board_game/checkers/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChessCheckers", name) + ui.open() + +/datum/board_game/checkers/tgui_static_data(mob/user) + return list("game_type" = "checkers") + +/datum/board_game/checkers/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + + return list( + "player_one" = player_one_mob, + "player_two" = player_two_mob, + "player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0), + "player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0), + "current_board" = current_board, + "selected_figure" = selected_figure, + "valid_moves" = valid_moves, + "game_state" = game_state, + "winner" = winner, + "has_won" = winner == ui.user.name, + "possible_jumps" = possible_jumps + ) + +/datum/board_game/checkers/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_player_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() == ui.user) + player_one = null + return TRUE + player_one = WEAKREF(ui.user) + return TRUE + if("be_player_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() == ui.user) + player_two = null + return TRUE + player_two = WEAKREF(ui.user) + return TRUE + if("swap_players") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + if("clear_game") + if(game_state == GAME_SETUP) + return FALSE + reset(TRUE) + return TRUE + if("start_game") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + current_board = get_defaultboard() + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + if("play_again") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + turn_start_time = world.time + return TRUE + if("play_again_swapped") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + turn_start_time = world.time + return TRUE + if("game_action") + if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE) + var/game_action = player_actions(params["action"], params["data"], ui.user, "w") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_TWO + turn_start_time = world.time + return TRUE + if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO) + var/game_action = player_actions(params["action"], params["data"], ui.user, "b") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + return FALSE + +/datum/board_game/checkers/proc/reset(full) + winner = null + player_one_time = 0 + player_two_time = 0 + selected_figure.Cut() + valid_moves.Cut() + if(full) + current_board.Cut() + game_state = GAME_SETUP + player_one = null + player_two = null + else + current_board = get_defaultboard() + game_state = GAME_PLAYER_ONE + +/datum/board_game/checkers/proc/player_actions(action, list/params, mob/user, active_color) + switch(action) + if("select_figure") + var/list/validated_data = validate_coords(params) + if (!validated_data) + return GAME_ACTION_NONE + + var/piece = current_board[validated_data[2]][validated_data[1]] + if(!piece || piece[1] != active_color) + return GAME_ACTION_NONE + + if(has_available_jumps(active_color)) + var/list/jumps = generate_valid_moves(validated_data[1], validated_data[2], TRUE) + if (length(jumps) == 0) + return GAME_ACTION_NONE + + selected_figure = validated_data + update_valid_moves() + return GAME_ACTION_SELECT + + if("move_figure") + var/list/coords = validate_coords(params) + if(!coords || !selected_figure) + return GAME_ACTION_NONE + + var/to_x = coords[1] + var/to_y = coords[2] + + if(!valid_move(to_x, to_y)) + return GAME_ACTION_NONE + + var/from_x = selected_figure[1] + var/from_y = selected_figure[2] + var/moving_piece = current_board[from_y][from_x] + + if(moving_piece[2] == "M" && ((moving_piece[1] == "w" && to_y == 1) || (moving_piece[1] == "b" && to_y == GRID_SIZE))) + moving_piece = moving_piece[1] + "K" + + current_board[to_y][to_x] = moving_piece + current_board[from_y][from_x] = null + + var/did_jump = FALSE + if(abs(to_x - from_x) >= 2 && abs(to_y - from_y) >= 2) + var/step_x = (to_x - from_x) / abs(to_x - from_x) + var/step_y = (to_y - from_y) / abs(to_y - from_y) + var/x = from_x + step_x + var/y = from_y + step_y + + while(x != to_x && y != to_y) + var/p = current_board[y][x] + if(p && p[1] != moving_piece[1]) + current_board[y][x] = null + did_jump = TRUE + break + x += step_x + y += step_y + + if(did_jump && piece_can_jump_again(to_x, to_y)) + selected_figure = list(to_x, to_y) + update_valid_moves() + validate_victory(active_color) + possible_jumps = get_mandatory_jumps(active_color) + return GAME_ACTION_SELECT + + var/turn_duration = world.time - turn_start_time + if(game_state == GAME_PLAYER_ONE) + player_one_time += turn_duration + else if(game_state == GAME_PLAYER_TWO) + player_two_time += turn_duration + + selected_figure.Cut() + update_valid_moves() + validate_victory(active_color) + if(game_state != GAME_OVER) + possible_jumps = get_mandatory_jumps(active_color == "w" ? "b" : "w") + else + possible_jumps.Cut() + return GAME_ACTION_END_TURN + +/datum/board_game/checkers/proc/get_mandatory_jumps(active_color) + var/list/jumping_pieces = list() + if(!has_available_jumps(active_color)) + return jumping_pieces + + for(var/row = 1 to GRID_SIZE) + for(var/col = 1 to GRID_SIZE) + var/piece = current_board[row][col] + if(piece && piece[1] == active_color) + var/list/jumps = generate_valid_moves(col, row, TRUE) + if(length(jumps) > 0) + UNTYPED_LIST_ADD(jumping_pieces, list(col, row)) + return jumping_pieces + +/datum/board_game/checkers/proc/update_valid_moves() + valid_moves.Cut() + if(length(selected_figure)) + var/x = selected_figure[1] + var/y = selected_figure[2] + valid_moves = generate_valid_moves(x, y) + +/datum/board_game/checkers/proc/piece_can_jump_again(x, y) + var/piece = current_board[y][x] + if(!piece) + return FALSE + + for(var/list/move in generate_valid_moves(x, y, TRUE)) + if(abs(move[1] - x) >= 2 && abs(move[2] - y) >= 2) + return TRUE + return FALSE + +/datum/board_game/checkers/proc/valid_move(to_x, to_y) + for(var/list/move in valid_moves) + if(move[1] == to_x && move[2] == to_y) + return TRUE + return FALSE + +/datum/board_game/checkers/proc/validate_victory(active_color) + var/opponent_color = active_color == "w" ? "b" : "w" + var/any_opponent_pieces = FALSE + var/opponent_has_moves = FALSE + + var/opponent_has_jumps = has_available_jumps(opponent_color) + + for(var/row = 1 to GRID_SIZE) + for(var/col = 1 to GRID_SIZE) + var/piece = current_board[row][col] + if(piece && piece[1] == opponent_color) + any_opponent_pieces = TRUE + + var/list/moves + if(opponent_has_jumps) + moves = generate_valid_moves(col, row, TRUE) + else + moves = generate_valid_moves(col, row) + + if(length(moves) > 0) + opponent_has_moves = TRUE + break + if(opponent_has_moves) + break + + if(!any_opponent_pieces) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + winner = active_color == "w" ? player_one_mob?.name : player_two_mob?.name + game_state = GAME_OVER + return + + if(!opponent_has_moves) + if(any_opponent_pieces) + winner = null + game_state = GAME_OVER_DRAW + return + + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + winner = active_color == "w" ? player_one_mob?.name : player_two_mob?.name + game_state = GAME_OVER + +/datum/board_game/checkers/proc/generate_valid_moves(x, y, jump_only) + var/piece = current_board[y][x] + var/list/moves = list() + + if (!piece) + return moves + + var/directions = list() + + switch(piece[2]) + if("M") + if(piece[1] == "w") + directions = list(list(1, -1), list(-1, -1)) + else + directions = list(list(1, 1), list(-1, 1)) + if("K") + directions = list(list(1, 1), list(-1, 1), list(1, -1), list(-1, -1)) + + var/list/jump_moves = list() + add_jumps(directions, x, y, jump_moves) + + if(length(jump_moves) || jump_only) + return jump_moves + + switch(piece[2]) + if("M") + for(var/list/direction in directions) + var/new_x = x + direction[1] + var/new_y = y + direction[2] + if(field_check(new_x, new_y) && current_board[new_y][new_x] == null) + UNTYPED_LIST_ADD(moves, list(new_x, new_y)) + if("K") + for(var/list/direction in directions) + var/step = 1 + while(TRUE) + var/new_x = x + direction[1] * step + var/new_y = y + direction[2] * step + if(!field_check(new_x, new_y)) + break + if(current_board[new_y][new_x]) + break + UNTYPED_LIST_ADD(moves, list(new_x, new_y)) + step += 1 + return moves + +/datum/board_game/checkers/proc/has_available_jumps(active_color) + for (var/row = 1 to GRID_SIZE) + for (var/col = 1 to GRID_SIZE) + var/piece = current_board[row][col] + if (piece && piece[1] == active_color) + var/list/jumps = generate_valid_moves(col, row, TRUE) + if (length(jumps) > 0) + return TRUE + return FALSE + +/datum/board_game/checkers/proc/already_visited(x, y, list/visited_list) + for(var/list/coord in visited_list) + if(coord[1] == x && coord[2] == y) + return TRUE + return FALSE + +/datum/board_game/checkers/proc/add_jumps(var/list/directions, start_x, start_y, list/jump_moves, list/visited = list()) + var/piece = current_board[start_y][start_x] + if(!piece) + return + + for(var/list/direction in directions) + switch(piece[2]) + if("M") + var/tx = start_x + direction[1] * 2 + var/ty = start_y + direction[2] * 2 + + if(!field_check(tx, ty) || already_visited(tx, ty, visited)) + continue + + if(can_jump_piece(start_x, start_y, tx, ty)) + var/list/new_visited = visited.Copy() + UNTYPED_LIST_ADD(new_visited, list(tx, ty)) + UNTYPED_LIST_ADD(jump_moves, list(tx, ty)) + add_jumps(directions, tx, ty, jump_moves, new_visited) + if("K") + var/step = 1 + while(TRUE) + var/tx = start_x + direction[1] * step + var/ty = start_y + direction[2] * step + + if(!field_check(tx, ty)) + break + + if(already_visited(tx, ty, visited)) + step++ + continue + + if(can_jump_piece(start_x, start_y, tx, ty)) + var/list/new_visited = visited.Copy() + UNTYPED_LIST_ADD(new_visited, list(tx, ty)) + UNTYPED_LIST_ADD(jump_moves, list(tx, ty)) + add_jumps(directions, tx, ty, jump_moves, new_visited) + + step++ + +/datum/board_game/checkers/proc/can_jump_piece(from_x, from_y, to_x, to_y) + var/piece = current_board[from_y][from_x] + if(!piece) + return FALSE + + var/dx = to_x - from_x + var/dy = to_y - from_y + var/color = piece[1] + + switch(piece[2]) + if("M") + if(abs(dx) != 2 || abs(dy) != 2) + return FALSE + var/mid_x = from_x + dx / 2 + var/mid_y = from_y + dy / 2 + var/mid_piece = current_board[mid_y][mid_x] + if(mid_piece && mid_piece[1] != color && current_board[to_y][to_x] == null) + return TRUE + return FALSE + if("K") + if(abs(dx) != abs(dy)) + return FALSE + + var/step_x = dx / abs(dx) + var/step_y = dy / abs(dy) + var/x = from_x + step_x + var/y = from_y + step_y + + var/jumped_piece_found = FALSE + var/jumped_x = 0 + var/jumped_y = 0 + + while(x != to_x && y != to_y) + var/piece_at_square = current_board[y][x] + if(piece_at_square) + if(piece_at_square[1] == color) + return FALSE + if(jumped_piece_found) + return FALSE + jumped_piece_found = TRUE + jumped_x = x + jumped_y = y + x += step_x + y += step_y + + if(!jumped_piece_found) + return FALSE + + if(current_board[to_y][to_x] != null) + return FALSE + + x = jumped_x + step_x + y = jumped_y + step_y + + while(x != to_x || y != to_y) + if(current_board[y][x] != null) + return FALSE + x += step_x + y += step_y + + return TRUE + +/datum/board_game/checkers/proc/field_check(new_x_location, new_y_location) + if(new_x_location < 1 || new_x_location > GRID_SIZE) + return FALSE + if(new_y_location < 1 || new_y_location > GRID_SIZE) + return FALSE + return TRUE + +/datum/board_game/checkers/proc/validate_coords(list/params) + var/x_loc = text2num(params["loc_x"]) + var/y_loc = text2num(params["loc_y"]) + + if(!isnum(x_loc) || !isnum(y_loc)) + return null + + if(!field_check(x_loc, y_loc)) + return null + + return list(x_loc, y_loc) + +/datum/board_game/checkers/proc/get_defaultboard() + var/list/deep_copy = list() + for(var/list/row in default_board) + UNTYPED_LIST_ADD(deep_copy, row.Copy()) + return deep_copy + +#undef GAME_PLAYER_ONE +#undef GAME_PLAYER_TWO +#undef GAME_OVER +#undef GAME_OVER_DRAW +#undef GRID_SIZE + +#undef GAME_ACTION_NONE +#undef GAME_ACTION_SELECT +#undef GAME_ACTION_END_TURN diff --git a/code/modules/casino/board_games/chess.dm b/code/modules/casino/board_games/chess.dm new file mode 100644 index 0000000000..b31522dd44 --- /dev/null +++ b/code/modules/casino/board_games/chess.dm @@ -0,0 +1,566 @@ +#define GAME_PLAYER_ONE 1 +#define GAME_PLAYER_TWO 2 +#define GAME_OVER 3 +#define GAME_OVER_DRAW 4 +#define GRID_SIZE 8 +#define GAME_FLAG_CHECK_PONE 0x1 +#define GAME_FLAG_CHECK_PTWO 0x2 +#define GAME_FLAG_CASTLING_USED_PONE 0x4 +#define GAME_FLAG_CASTLING_USED_PTWO 0x8 + +#define GAME_ACTION_NONE 0 +#define GAME_ACTION_SELECT 1 +#define GAME_ACTION_END_TURN 2 + +/obj/structure/casino_table/board_game/chess + name = GAME_CHESS + desc = "A classic chess game." + icon_state = "gamble_chess" + game_ui = /datum/board_game/chess + +/datum/board_game/chess + name = GAME_CHESS + table_icon = "gamble_chess" + var/datum/weakref/player_one + var/datum/weakref/player_two + var/player_one_time = 0 + var/player_two_time = 0 + var/static/list/default_board = list( + list("bR","bN","bB","bQ","bK","bB","bN","bR"), + list("bP","bP","bP","bP","bP","bP","bP","bP"), + list(null,null,null,null,null,null,null,null), + list(null,null,null,null,null,null,null,null), + list(null,null,null,null,null,null,null,null), + list(null,null,null,null,null,null,null,null), + list("wP","wP","wP","wP","wP","wP","wP","wP"), + list("wR","wN","wB","wQ","wK","wB","wN","wR") + ) + var/list/current_board = list() + var/list/valid_moves = list() + var/list/selected_figure = list() + var/list/last_double_pawn_move = list() + var/game_flags = NONE + var/turn_start_time = 0 + var/winner + +/datum/board_game/chess/Destroy(force) + player_one = null + player_two = null + . = ..() + +/datum/board_game/chess/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ChessCheckers", name) + ui.open() + +/datum/board_game/chess/tgui_static_data(mob/user) + return list("game_type" = "chess") + +/datum/board_game/chess/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + + return list( + "player_one" = player_one_mob, + "player_two" = player_two_mob, + "player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0), + "player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0), + "current_board" = current_board, + "selected_figure" = selected_figure, + "valid_moves" = valid_moves, + "game_state" = game_state, + "winner" = winner, + "has_won" = winner == ui.user.name, + "game_flags" = game_flags + ) + +/datum/board_game/chess/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_player_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() == ui.user) + player_one = null + return TRUE + player_one = WEAKREF(ui.user) + return TRUE + if("be_player_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() == ui.user) + player_two = null + return TRUE + player_two = WEAKREF(ui.user) + return TRUE + if("swap_players") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + if("clear_game") + if(game_state == GAME_SETUP) + return FALSE + reset(TRUE) + return TRUE + if("start_game") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + current_board = get_defaultboard() + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + if("play_again") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + turn_start_time = world.time + return TRUE + if("play_again_swapped") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + turn_start_time = world.time + return TRUE + if("game_action") + if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE) + var/game_action = player_actions(params["action"], params["data"], ui.user, "w") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_TWO + turn_start_time = world.time + return TRUE + if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO) + var/game_action = player_actions(params["action"], params["data"], ui.user, "b") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + return FALSE + +/datum/board_game/chess/proc/reset(full) + winner = null + player_one_time = 0 + player_two_time = 0 + selected_figure.Cut() + valid_moves.Cut() + last_double_pawn_move.Cut() + game_flags = NONE + if(full) + current_board.Cut() + game_state = GAME_SETUP + player_one = null + player_two = null + else + current_board = get_defaultboard() + game_state = GAME_PLAYER_ONE + +/datum/board_game/chess/proc/player_actions(action, list/params, mob/user, active_color) + switch(action) + if("select_figure") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return GAME_ACTION_NONE + + var/piece = current_board[validated_data[2]][validated_data[1]] + if(!piece) + return GAME_ACTION_NONE + + if(piece[1] != active_color) + return GAME_ACTION_NONE + + selected_figure = validated_data + update_valid_moves() + return GAME_ACTION_SELECT + if("move_figure") + var/list/coords = validate_coords(params) + if(!coords || !selected_figure) + return GAME_ACTION_NONE + + var/from_x = selected_figure[1] + var/from_y = selected_figure[2] + var/to_x = coords[1] + var/to_y = coords[2] + + if(!valid_move(to_x, to_y)) + return GAME_ACTION_NONE + + var/moving_piece = current_board[from_y][from_x] + var/target_piece = current_board[to_y][to_x] + + var/turn_duration = world.time - turn_start_time + if(game_state == GAME_PLAYER_ONE) + player_one_time += turn_duration + else if(game_state == GAME_PLAYER_TWO) + player_two_time += turn_duration + + if(moving_piece[2] == "K" && abs(to_x - from_x) == 2) + if(!handle_castling(active_color, from_x, from_y, to_x, to_y)) + return GAME_ACTION_NONE + do_castling(active_color, from_x, from_y, to_x, to_y) + + if(moving_piece[2] == "P" && abs(to_x - from_x) == 1 && !target_piece) + if(can_en_passant(from_x, from_y, to_x, to_y)) + current_board[from_y][to_x] = null + + if(!(moving_piece[2] == "K" && abs(to_x - from_x) == 2)) + current_board[to_y][to_x] = moving_piece + current_board[from_y][from_x] = null + + if(moving_piece[2] == "P" && abs(to_y - from_y) == 2) + last_double_pawn_move = list(to_x, to_y, moving_piece[1]) + else + last_double_pawn_move.Cut() + + if(moving_piece[2] == "P") + if((moving_piece[1] == "w" && to_y == 1) || (moving_piece[1] == "b" && to_y == GRID_SIZE)) + var/promotion = params["promotion_choice"] + if(!(promotion in list("Q", "R", "B", "N"))) + promotion = "Q" + current_board[to_y][to_x] = moving_piece[1] + promotion + + selected_figure.Cut() + update_valid_moves() + validate_victory(active_color) + + return GAME_ACTION_END_TURN + +/datum/board_game/chess/proc/valid_move(to_x, to_y) + for(var/list/move in valid_moves) + if(move[1] == to_x && move[2] == to_y) + return TRUE + return FALSE + +/datum/board_game/chess/proc/handle_castling(active_color, from_x, from_y, to_x, to_y) + if(active_color == "w" && (game_flags & (GAME_FLAG_CASTLING_USED_PONE|GAME_FLAG_CHECK_PONE))) + return FALSE + if(active_color == "b" && (game_flags & (GAME_FLAG_CASTLING_USED_PTWO|GAME_FLAG_CHECK_PTWO))) + return FALSE + + var/king = current_board[from_y][from_x] + + if(king != "wK" && king != "bK") + return FALSE + + if(abs(to_x - from_x) != 2) + return FALSE + + var/row = from_y + var/rook_x + var/rook + var/col_start + var/col_end + + if(to_x > from_x) + rook_x = GRID_SIZE + rook = current_board[row][rook_x] + if(rook != "wR" && rook != "bR") + return FALSE + col_start = from_x + 1 + col_end = to_x + else + rook_x = 1 + rook = current_board[row][rook_x] + if(rook != "wR" && rook != "bR") + return FALSE + col_start = to_x + col_end = from_x - 1 + + for(var/c = col_start to col_end) + if(current_board[row][c] != null && c != from_x) + return FALSE + + var/opponent_color = active_color == "w" ? "b" : "w" + if(!opponent_color) + return FALSE + + if(square_under_attack(opponent_color, from_x, from_y)) + return FALSE + + var/step = (to_x > from_x ? 1 : -1) + var/x = from_x + while(x != to_x + step) + if(x != from_x && x != to_x && square_under_attack(opponent_color, x, row)) + return FALSE + x += step + return TRUE + +/datum/board_game/chess/proc/do_castling(active_color, from_x, from_y, to_x, to_y) + var/row = from_y + var/king = current_board[from_y][from_x] + var/rook_x + var/rook + var/rook_target_x + + if(to_x > from_x) + rook_x = GRID_SIZE + rook = current_board[row][rook_x] + rook_target_x = to_x - 1 + else + rook_x = 1 + rook = current_board[row][rook_x] + rook_target_x = to_x + 1 + + current_board[row][to_x] = king + current_board[row][from_x] = null + + current_board[row][rook_target_x] = rook + current_board[row][rook_x] = null + + if(active_color == "w") + game_flags |= GAME_FLAG_CASTLING_USED_PONE + else + game_flags |= GAME_FLAG_CASTLING_USED_PTWO + + return TRUE + +/datum/board_game/chess/proc/update_valid_moves() + valid_moves.Cut() + if(length(selected_figure)) + var/x = selected_figure[1] + var/y = selected_figure[2] + valid_moves = generate_valid_moves(x, y) + +/datum/board_game/chess/proc/square_under_attack(opponent_color, x, y) + for(var/row = 1; row <= GRID_SIZE; row++) + for(var/col = 1; col <= GRID_SIZE; col++) + var/piece = current_board[row][col] + if(!piece) + continue + + if(piece[1] != opponent_color) + continue + + var/dx = x - col + var/dy = y - row + + switch(piece[2]) + if("P") + var/dir = (piece[1] == "w" ? -1 : 1) + if(dy == dir && abs(dx) == 1) + return TRUE + if("N") + if(abs(dx) == 2 && abs(dy) == 1 || abs(dx) == 1 && abs(dy) == 2) + return TRUE + if("B") + if(abs(dx) == abs(dy) && path_clear(col, row, x, y)) + return TRUE + if("R") + if((dx == 0 || dy == 0) && path_clear(col, row, x, y)) + return TRUE + if("Q") + if((dx == 0 || dy == 0 || abs(dx) == abs(dy)) && path_clear(col, row, x, y)) + return TRUE + if("K") + if(abs(dx) <= 1 && abs(dy) <= 1) + return TRUE + return FALSE + +/datum/board_game/chess/proc/path_clear(from_x, from_y, to_x, to_y) + var/dx = to_x - from_x + var/dy = to_y - from_y + var/step_x = dx == 0 ? 0 : (dx / abs(dx)) + var/step_y = dy == 0 ? 0 : (dy / abs(dy)) + var/x = from_x + step_x + var/y = from_y + step_y + + while(x != to_x || y != to_y) + if(!field_check(x, y)) + return FALSE + if(current_board[y][x] != null) + return FALSE + x += step_x + y += step_y + + return TRUE + +/datum/board_game/chess/proc/validate_victory(active_color) + var/opponent_color = active_color == "w" ? "b" : "w" + + var/king_pos = locate_king(opponent_color) + if(!king_pos) + var/mob/winning_player = (active_color == "w" ? player_two?.resolve() : player_one?.resolve()) + winner = winning_player?.name + game_state = GAME_OVER + return + + var/in_check = square_under_attack(active_color, king_pos[1], king_pos[2]) + + if(in_check) + game_flags |= (opponent_color == "w" ? GAME_FLAG_CHECK_PONE : GAME_FLAG_CHECK_PTWO) + else + game_flags &= ~(opponent_color == "w" ? GAME_FLAG_CHECK_PONE : GAME_FLAG_CHECK_PTWO) + + var/no_moves = TRUE + for(var/row = 1 to GRID_SIZE) + for(var/col = 1 to GRID_SIZE) + var/piece = current_board[row][col] + if(!piece) + continue + if(piece[1] != opponent_color) + continue + var/list/moves = generate_valid_moves(col, row) + if(length(moves) > 0) + no_moves = FALSE + + if(in_check && no_moves) + var/mob/winning_player = active_color == "w" ? player_one?.resolve() : player_two?.resolve() + winner = winning_player?.name + game_state = GAME_OVER + return + if(!in_check && no_moves) + winner = null + game_state = GAME_OVER_DRAW + +/datum/board_game/chess/proc/locate_king(color) + var/king_symbol = color + "K" + for(var/row = 1 to GRID_SIZE) + for(var/col = 1 to GRID_SIZE) + if(current_board[row][col] == king_symbol) + return list(col, row) + return null + +/datum/board_game/chess/proc/generate_valid_moves(x, y) + var/piece = current_board[y][x] + var/moves = list() + if(!piece) + return moves + + for(var/row = 1 to GRID_SIZE) + for(var/col = 1 to GRID_SIZE) + if(row == y && col == x) + continue + if(can_move_piece(x, y, col, row)) + UNTYPED_LIST_ADD(moves, list(col, row)) + return moves + +/datum/board_game/chess/proc/can_move_piece(from_x, from_y, to_x, to_y) + var/piece = current_board[from_y][from_x] + if(!piece) + return FALSE + + var/dx = to_x - from_x + var/dy = to_y - from_y + var/abs_dx = abs(dx) + var/abs_dy = abs(dy) + + if(dx == 0 && dy == 0) + return FALSE + + var/color = piece[1] + var/target = current_board[to_y][to_x] + if(target && target[1] == color) + return FALSE + + var/can_move = FALSE + + switch(piece[2]) + if("P") + var/dir = (color == "w" ? -1 : 1) + if(dx == 0 && dy == dir && !target) + can_move = TRUE + else if(dx == 0 && dy == 2*dir && !target && current_board[from_y + dir][from_x] == null && ((color == "w" && from_y == 7) || (color == "b" && from_y == 2))) + can_move = TRUE + else if(abs_dx == 1 && dy == dir && target) + can_move = TRUE + else if(abs_dx == 1 && dy == dir && !target && length(last_double_pawn_move)) + if(last_double_pawn_move[1] == to_x && last_double_pawn_move[2] == from_y && last_double_pawn_move[3] == (color == "w" ? "b" : "w")) + can_move = TRUE + if("N") + if((abs_dx == 2 && abs_dy == 1) || (abs_dx == 1 && abs_dy == 2)) + can_move = TRUE + if("B") + if(abs_dx == abs_dy && path_clear(from_x, from_y, to_x, to_y)) + can_move = TRUE + if("R") + if((dx == 0 || dy == 0) && path_clear(from_x, from_y, to_x, to_y)) + can_move = TRUE + if("Q") + if((dx == 0 || dy == 0 || abs_dx == abs_dy) && path_clear(from_x, from_y, to_x, to_y)) + can_move = TRUE + if("K") + if(abs_dx <= 1 && abs_dy <= 1) + can_move = TRUE + else if(abs_dx == 2 && dy == 0) + can_move = handle_castling(color, from_x, from_y, to_x, to_y) + + if(!can_move) + return FALSE + + var/orig_piece = current_board[to_y][to_x] + current_board[to_y][to_x] = piece + current_board[from_y][from_x] = null + + var/king_pos = locate_king(color) + if(!king_pos) + current_board[from_y][from_x] = piece + current_board[to_y][to_x] = orig_piece + return FALSE + + var/in_check = square_under_attack(color == "w" ? "b" : "w", king_pos[1], king_pos[2]) + + current_board[from_y][from_x] = piece + current_board[to_y][to_x] = orig_piece + + return !in_check + +/datum/board_game/chess/proc/can_en_passant(from_x, from_y, to_x, to_y) + if(!last_double_pawn_move) + return FALSE + return last_double_pawn_move[1] == to_x && last_double_pawn_move[2] == from_y && last_double_pawn_move[3] == (current_board[from_y][from_x][1] == "w" ? "b" : "w") + +/datum/board_game/chess/proc/field_check(new_x_location, new_y_location) + if(new_x_location < 1 || new_x_location > GRID_SIZE) + return FALSE + if(new_y_location < 1 || new_y_location > GRID_SIZE) + return FALSE + return TRUE + +/datum/board_game/chess/proc/validate_coords(list/params) + var/x_loc = text2num(params["loc_x"]) + var/y_loc = text2num(params["loc_y"]) + + if(!isnum(x_loc) || !isnum(y_loc)) + return null + + if(!field_check(x_loc, y_loc)) + return null + + return list(x_loc, y_loc) + +/datum/board_game/chess/proc/get_defaultboard() + var/list/deep_copy = list() + for(var/list/row in default_board) + UNTYPED_LIST_ADD(deep_copy, row.Copy()) + return deep_copy + +#undef GAME_PLAYER_ONE +#undef GAME_PLAYER_TWO +#undef GAME_OVER +#undef GAME_OVER_DRAW +#undef GRID_SIZE +#undef GAME_FLAG_CHECK_PONE +#undef GAME_FLAG_CHECK_PTWO +#undef GAME_FLAG_CASTLING_USED_PONE +#undef GAME_FLAG_CASTLING_USED_PTWO + +#undef GAME_ACTION_NONE +#undef GAME_ACTION_SELECT +#undef GAME_ACTION_END_TURN diff --git a/code/modules/casino/board_games/four_in_a_row.dm b/code/modules/casino/board_games/four_in_a_row.dm new file mode 100644 index 0000000000..a8f0db7f90 --- /dev/null +++ b/code/modules/casino/board_games/four_in_a_row.dm @@ -0,0 +1,330 @@ +#define GAME_PLAYER_ONE 1 +#define GAME_PLAYER_TWO 2 +#define GAME_OVER 3 +#define GAME_OVER_DRAW 4 + +/obj/structure/casino_table/board_game/four_row + name = GAME_FOUR_ROW + desc = "A game where two players need to connect their chips in a row." + icon_state = "gamble_four" + game_ui = /datum/board_game/four_row + + +/datum/board_game/four_row + name = GAME_FOUR_ROW + table_icon = "gamble_four" + var/datum/weakref/player_one + var/datum/weakref/player_two + var/list/placed_chips_pone = list() + var/list/placed_chips_ptwo = list() + var/list/winning_tiles = list() + var/grid_x_size = 7 + var/grid_y_size = 6 + var/win_count = 4 + var/player_one_color = "yellow" + var/player_two_color = "red" + var/winner + var/static/list/possible_colors = list("red", "yellow", "green", "orange", "blue", "cyan") + +/datum/board_game/four_row/Destroy(force) + player_one = null + player_two = null + . = ..() + +/datum/board_game/four_row/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "FourInARow", name) + ui.open() + +/datum/board_game/four_row/tgui_static_data(mob/user) + return list( + "colors" = possible_colors + ) + +/datum/board_game/four_row/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + + return list( + "player_one" = player_one_mob, + "player_two" = player_two_mob, + "placed_chips_pone" = placed_chips_pone, + "placed_chips_ptwo" = placed_chips_ptwo, + "game_state" = game_state, + "grid_x_size" = grid_x_size, + "grid_y_size" = grid_y_size, + "player_one_color" = player_one_color, + "player_two_color" = player_two_color, + "win_count" = win_count, + "winner" = winner, + "has_won" = winner == ui.user.name, + "winning_tiles" = winning_tiles, + ) + +/datum/board_game/four_row/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_player_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() == ui.user) + player_one = null + return TRUE + player_one = WEAKREF(ui.user) + return TRUE + if("be_player_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() == ui.user) + player_two = null + return TRUE + player_two = WEAKREF(ui.user) + return TRUE + if("swap_players") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + if("set_color_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() != ui.user) + return FALSE + var/new_color = params["color"] + if(new_color == player_two_color) + return FALSE + if(!(new_color in possible_colors)) + return FALSE + player_one_color = new_color + return TRUE + if("set_color_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() != ui.user) + return FALSE + var/new_color = params["color"] + if(new_color == player_one_color) + return FALSE + if(!(new_color in possible_colors)) + return FALSE + player_two_color = new_color + return TRUE + if("change_size") + if(game_state != GAME_SETUP) + return FALSE + var/new_size = text2num(params["size"]) + return set_new_size(new_size) + if("change_win") + if(game_state != GAME_SETUP) + return FALSE + return change_win_count(text2num(params["count"])) + if("clear_game") + if(game_state == GAME_SETUP) + return FALSE + reset(TRUE) + return TRUE + if("start_game") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + game_state = GAME_PLAYER_ONE + return TRUE + if("play_again") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + return TRUE + if("play_again_swapped") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + return TRUE + if("game_action") + if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE) + if(player_actions(params["action"], params["data"], ui.user)) + if(game_state < GAME_OVER) + game_state = GAME_PLAYER_TWO + return TRUE + if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO) + if(player_actions(params["action"], params["data"], ui.user)) + if(game_state < GAME_OVER) + game_state = GAME_PLAYER_ONE + return TRUE + return FALSE + +/datum/board_game/four_row/proc/change_win_count(new_count) + if(!isnum(new_count)) + return FALSE + if(new_count < 4 || new_count > 5) + return FALSE + win_count = new_count + return TRUE + +/datum/board_game/four_row/proc/reset(full) + placed_chips_pone.Cut() + placed_chips_ptwo.Cut() + winning_tiles.Cut() + winner = null + if(full) + game_state = GAME_SETUP + player_one = null + player_two = null + else + game_state = GAME_PLAYER_ONE + +/datum/board_game/four_row/proc/player_actions(action, list/params, mob/user) + switch(action) + if("place_chip") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + var/x_loc = validated_data[2] + + var/target_y = 0 + for(var/y = grid_y_size; y >= 1; y--) + var/key = "[x_loc],[y]" + if(!placed_chips_pone[key] && !placed_chips_ptwo[key]) + target_y = y + break + + if(target_y == 0) + return FALSE + + var/key = "[x_loc],[target_y]" + + if(game_state == GAME_PLAYER_ONE) + placed_chips_pone[key] = TRUE + else + placed_chips_ptwo[key] = TRUE + + validate_victory(x_loc, target_y, user.name) + return TRUE + + +/datum/board_game/four_row/proc/has_chip(list/player_list, x, y) + return player_list["[x],[y]"] + +/datum/board_game/four_row/proc/collect_direction(list/player_list, start_x, start_y, dx, dy) + var/list/tiles = list() + var/x = start_x + var/y = start_y + + while(field_check(x, y) && has_chip(player_list, x, y)) + tiles += "[x],[y]" + x += dx + y += dy + + return tiles + +/datum/board_game/four_row/proc/validate_victory(x, y, user_name) + var/list/current_list + + if(game_state == GAME_PLAYER_ONE) + current_list = placed_chips_pone + else + current_list = placed_chips_ptwo + + var/list/h1 = collect_direction(current_list, x, y, 1, 0) + var/list/h2 = collect_direction(current_list, x, y, -1, 0) + var/list/horizontal = h1 + h2 + horizontal -= "[x],[y]" + + if(length(horizontal) >= win_count) + winning_tiles = horizontal + game_state = GAME_OVER + winner = user_name + return + + var/list/v1 = collect_direction(current_list, x, y, 0, 1) + var/list/v2 = collect_direction(current_list, x, y, 0, -1) + var/list/vertical = v1 + v2 + vertical -= "[x],[y]" + + if(length(vertical) >= win_count) + winning_tiles = vertical + game_state = GAME_OVER + winner = user_name + return + + var/list/d1 = collect_direction(current_list, x, y, 1, 1) + var/list/d2 = collect_direction(current_list, x, y, -1, -1) + var/list/diag1 = d1 + d2 + diag1 -= "[x],[y]" + + if(length(diag1) >= win_count) + winning_tiles = diag1 + game_state = GAME_OVER + winner = user_name + return + + var/list/d3 = collect_direction(current_list, x, y, 1, -1) + var/list/d4 = collect_direction(current_list, x, y, -1, 1) + var/list/diag2 = d3 + d4 + diag2 -= "[x],[y]" + + if(length(diag2) >= win_count) + winning_tiles = diag2 + game_state = GAME_OVER + winner = user_name + return + check_draw() + +/datum/board_game/four_row/proc/check_draw() + for(var/x = 1 to grid_x_size) + for(var/y = 1 to grid_y_size) + var/key = "[x],[y]" + if(!placed_chips_pone[key] && !placed_chips_ptwo[key]) + return FALSE + + winner = null + game_state = GAME_OVER_DRAW + return TRUE + +/datum/board_game/four_row/proc/set_new_size(new_size) + if(!isnum(new_size)) + return FALSE + if(new_size < 5 || new_size > 10) + return FALSE + grid_x_size = new_size + grid_y_size = new_size - 1 + return TRUE + +/datum/board_game/four_row/proc/field_check(new_x_location, new_y_location) + if(new_x_location < 1 || new_x_location > grid_x_size) + return FALSE + if(new_y_location < 1 || new_y_location > grid_y_size) + return FALSE + return TRUE + +/datum/board_game/four_row/proc/validate_coords(list/params) + var/x_loc = text2num(params["loc_x"]) + var/y_loc = text2num(params["loc_y"]) + + if(!isnum(x_loc) || !isnum(y_loc)) + return null + + if(!field_check(x_loc, y_loc)) + return null + + var/key = "[x_loc],[y_loc]" + return list(key, x_loc, y_loc) + +#undef GAME_PLAYER_ONE +#undef GAME_PLAYER_TWO +#undef GAME_OVER +#undef GAME_OVER_DRAW diff --git a/code/modules/casino/board_games/nine_mens.dm b/code/modules/casino/board_games/nine_mens.dm new file mode 100644 index 0000000000..5c8ccea995 --- /dev/null +++ b/code/modules/casino/board_games/nine_mens.dm @@ -0,0 +1,421 @@ +#define GAME_PLAYER_ONE 1 +#define GAME_PLAYER_TWO 2 +#define GAME_OVER 3 +#define GAME_OVER_DRAW 4 +#define NODE_COUNT 24 + +#define GAME_ACTION_NONE 0 +#define GAME_ACTION_SELECT 1 +#define GAME_ACTION_END_TURN 2 + +#define GAME_PHASE_PLACING 0 +#define GAME_PHASE_MOVING 1 +#define GAME_PHASE_REMOVING 2 +#define GAME_PHASE_NONE 3 + +/obj/structure/casino_table/board_game/nine_mens + name = GAME_NINE_MENS_MORRIS + desc = "A classic nine_mens game." + icon_state = "gamble_ninemen" + game_ui = /datum/board_game/nine_mens + +/datum/board_game/nine_mens + name = GAME_NINE_MENS_MORRIS + table_icon = "gamble_ninemen" + var/datum/weakref/player_one + var/datum/weakref/player_two + var/player_one_time = 0 + var/player_two_time = 0 + var/list/current_board = list( + null, null, null, + null, null, null, + null, null, null, + null, null, null, + null, null, null, + null, null, null, + null, null, null, + null, null, null + ) + var/static/list/adjacent = list( + list(2, 10), + list(1, 3, 5), + list(2, 15), + list(5, 11), + list(2, 4, 6, 8), + list(5, 14), + list(8, 12), + list(5, 7, 9), + list(8, 13), + list(1, 11, 22), + list(4, 10, 12, 19), + list(7, 11, 16), + list(9, 14, 18), + list(6, 13, 15, 21), + list(3, 14, 24), + list(12, 17), + list(16, 18, 20), + list(13, 17), + list(11, 20), + list(17, 19, 21, 23), + list(14, 20), + list(10, 23), + list(20, 22, 24), + list(15, 23) + ) + var/static/list/mills = list( + list(1,2,3), + list(4,5,6), + list(7,8,9), + list(10,11,12), + list(13,14,15), + list(16,17,18), + list(19,20,21), + list(22,23,24), + + list(1,10,22), + list(4,11,19), + list(7,12,16), + list(2,5,8), + list(17,20,23), + list(9,13,18), + list(6,14,21), + list(3,15,24) + ) + var/list/valid_moves = list() + var/list/valid_removes = list() + var/selected_node + var/turn_start_time = 0 + var/pone_pieces = 9 + var/ptwo_pieces = 9 + var/winner + var/phase = GAME_PHASE_PLACING + +/datum/board_game/nine_mens/Destroy(force) + player_one = null + player_two = null + . = ..() + +/datum/board_game/nine_mens/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NineMen", name) + ui.open() + +/datum/board_game/nine_mens/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + + return list( + "player_one" = player_one_mob, + "player_two" = player_two_mob, + "player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0), + "player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0), + "current_board" = current_board, + "selected_node" = selected_node, + "valid_moves" = valid_moves, + "valid_removes" = valid_removes, + "game_state" = game_state, + "winner" = winner, + "has_won" = winner == ui.user.name, + "phase" = phase, + "pone_pieces" = pone_pieces, + "ptwo_pieces" = ptwo_pieces + ) + +/datum/board_game/nine_mens/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_player_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() == ui.user) + player_one = null + return TRUE + player_one = WEAKREF(ui.user) + return TRUE + if("be_player_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() == ui.user) + player_two = null + return TRUE + player_two = WEAKREF(ui.user) + return TRUE + if("swap_players") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + if("clear_game") + if(game_state == GAME_SETUP) + return FALSE + reset(TRUE) + return TRUE + if("start_game") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + if("play_again") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + turn_start_time = world.time + return TRUE + if("play_again_swapped") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + turn_start_time = world.time + return TRUE + if("game_action") + if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE) + var/game_action = player_actions(params["action"], params["data"], ui.user, "w") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_TWO + turn_start_time = world.time + return TRUE + if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO) + var/game_action = player_actions(params["action"], params["data"], ui.user, "b") + if(game_action) + if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN) + game_state = GAME_PLAYER_ONE + turn_start_time = world.time + return TRUE + return FALSE + +/datum/board_game/nine_mens/proc/reset(full) + winner = null + player_one_time = 0 + player_two_time = 0 + selected_node = null + valid_moves.Cut() + valid_removes.Cut() + pone_pieces = 9 + ptwo_pieces = 9 + phase = GAME_PHASE_PLACING + for(var/i = 1 to NODE_COUNT) + current_board[i] = null + if(full) + game_state = GAME_SETUP + player_one = null + player_two = null + else + game_state = GAME_PLAYER_ONE + +/datum/board_game/nine_mens/proc/player_actions(action, list/params, mob/user, active_color) + switch(action) + if("select_figure") + var/node = validate_node(params) + if(!node) + return GAME_ACTION_NONE + + var/piece = current_board[node] + if(!piece || piece[1] != active_color) + return GAME_ACTION_NONE + + selected_node = node + update_valid_moves() + return GAME_ACTION_SELECT + + if("move_figure") + var/target_node = validate_node(params) + if(!target_node) + return GAME_ACTION_NONE + + switch(phase) + if(GAME_PHASE_PLACING) + if(current_board[target_node]) + return GAME_ACTION_NONE + + current_board[target_node] = active_color + + if(active_color == "w") + if(pone_pieces > 0) + pone_pieces-- + else + if(ptwo_pieces > 0) + ptwo_pieces-- + + if(check_for_mill(target_node, active_color)) + phase = GAME_PHASE_REMOVING + update_valid_removals(active_color == "w" ? "b" : "w") + else + if((ptwo_pieces && active_color == "w") || (pone_pieces && active_color == "b")) + phase = GAME_PHASE_PLACING + else + phase = GAME_PHASE_MOVING + return phase == GAME_PHASE_REMOVING ? GAME_ACTION_SELECT : GAME_ACTION_END_TURN + + if(GAME_PHASE_MOVING) + if(!selected_node) + return GAME_ACTION_NONE + if(!valid_move(active_color, selected_node, target_node)) + return GAME_ACTION_NONE + + current_board[target_node] = current_board[selected_node] + current_board[selected_node] = null + selected_node = null + valid_moves.Cut() + validate_victory(active_color) + if(game_state < GAME_OVER) + if(check_for_mill(target_node, active_color)) + phase = GAME_PHASE_REMOVING + update_valid_removals(active_color == "w" ? "b" : "w") + else + phase = GAME_PHASE_MOVING + else + phase = GAME_PHASE_NONE + + return phase == GAME_PHASE_REMOVING ? GAME_ACTION_SELECT : GAME_ACTION_END_TURN + + if(GAME_PHASE_REMOVING) + if(!valid_remove(target_node)) + return GAME_ACTION_NONE + + current_board[target_node] = null + + valid_removes.Cut() + validate_victory(active_color) + if(game_state < GAME_OVER) + if((ptwo_pieces && active_color == "w") || (pone_pieces && active_color == "b")) + phase = GAME_PHASE_PLACING + else + phase = GAME_PHASE_MOVING + else + phase = GAME_PHASE_NONE + + return GAME_ACTION_END_TURN + + return GAME_ACTION_NONE + +/datum/board_game/nine_mens/proc/update_valid_moves() + valid_moves.Cut() + if(selected_node) + valid_moves = generate_valid_moves(selected_node) + +/datum/board_game/nine_mens/proc/update_valid_removals(opponent_color) + valid_removes.Cut() + for(var/i = 1 to NODE_COUNT) + if(current_board[i] && current_board[i][1] == opponent_color) + if(!check_for_mill(i, opponent_color)) + valid_removes += i + if(!length(valid_removes)) + for(var/i = 1 to NODE_COUNT) + if(current_board[i] && current_board[i][1] == opponent_color) + valid_removes += i + +/datum/board_game/nine_mens/proc/valid_remove(picked_node) + if(!picked_node || !current_board[picked_node] || !(picked_node in valid_removes)) + return FALSE + return TRUE + +/datum/board_game/nine_mens/proc/check_for_mill(node, color) + for(var/mill in mills) + if(node in mill) + var/mill_complete = TRUE + for(var/mill_node in mill) + if(!current_board[mill_node] || current_board[mill_node][1] != color) + mill_complete = FALSE + break + if(mill_complete) + return TRUE + return FALSE + +/datum/board_game/nine_mens/proc/valid_move(active_color, from_node, to_node) + if(current_board[to_node]) + return FALSE + + if(count_pieces(active_color) == 3) + return TRUE + + for(var/adj in adjacent[from_node]) + if(adj == to_node) + return TRUE + + return FALSE + +/datum/board_game/nine_mens/proc/validate_victory(active_color) + var/opponent = (active_color == "w") ? "b" : "w" + + if(count_pieces(opponent) < 3 || !has_moves(opponent)) + var/mob/winning_player = (active_color == "w" ? player_one?.resolve() : player_two?.resolve()) + winner = winning_player?.name + game_state = GAME_OVER + +/datum/board_game/nine_mens/proc/generate_valid_moves(node) + var/list/moves = list() + + if(!current_board[node]) + return moves + + var/color = current_board[node][1] + + if(count_pieces(color) == 3) + for(var/i = 1 to NODE_COUNT) + if(!current_board[i]) + moves += i + return moves + + for(var/adj in adjacent[node]) + if(!current_board[adj]) + moves += adj + + return moves + +/datum/board_game/nine_mens/proc/count_pieces(color) + var/count = color == "w" ? pone_pieces : ptwo_pieces + for(var/i = 1 to NODE_COUNT) + if(current_board[i] && current_board[i][1] == color) + count++ + return count + +/datum/board_game/nine_mens/proc/has_moves(color) + for(var/i = 1 to NODE_COUNT) + if(current_board[i] && current_board[i][1] == color) + if(length(generate_valid_moves(i))) + return TRUE + return FALSE + +/datum/board_game/nine_mens/proc/validate_node(list/params) + var/node = text2num(params["node_number"]) + + if(!isnum(node)) + return null + + if(node < 1 || node > 24) + return null + + return node + +#undef GAME_PLAYER_ONE +#undef GAME_PLAYER_TWO +#undef GAME_OVER +#undef GAME_OVER_DRAW +#undef NODE_COUNT + +#undef GAME_ACTION_NONE +#undef GAME_ACTION_SELECT +#undef GAME_ACTION_END_TURN + +#undef GAME_PHASE_PLACING +#undef GAME_PHASE_MOVING +#undef GAME_PHASE_REMOVING +#undef GAME_PHASE_NONE diff --git a/code/modules/casino/board_games/rpg_dice.dm b/code/modules/casino/board_games/rpg_dice.dm new file mode 100644 index 0000000000..2f4e36534f --- /dev/null +++ b/code/modules/casino/board_games/rpg_dice.dm @@ -0,0 +1,72 @@ +/obj/structure/casino_table/board_game/rpg_dice + name = GAME_RGP_DICE + desc = "A set of dice to roll with your friends." + icon_state = "gamble_dice" + game_ui = /datum/board_game/rpg_dice + +/datum/board_game/rpg_dice + name = GAME_RGP_DICE + table_icon = "gamble_dice" + var/list/last_rolls = list() + +/datum/board_game/rpg_dice/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "RpgDice", name) + ui.open() + +/datum/board_game/rpg_dice/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + return list( + "last_rolls" = last_rolls, + ) + +/datum/board_game/rpg_dice/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("roll_dice") + var/dice_size = params["dice_size"] + if(!isnum(dice_size) || dice_size > 10000) + return FALSE + var/dice_count = params["dice_count"] + if(!isnum(dice_count) || dice_count < 1 || dice_count > 10) + dice_count = 1 + var/modifier = params["dice_mod"] + var/list/results = list() + var/sum = 0 + if(!isnum(modifier)) + modifier = 0 + var/apply_to_all = params["mod_all"] + for(var/dice in 1 to dice_count) + var/result = rand(1, dice_size) + UNTYPED_LIST_ADD(results, list("result" = result, "state" = check_crit(1, dice_size, result))) + sum += result + sum += apply_to_all ? modifier * dice_count : modifier + UNTYPED_LIST_ADD(last_rolls, list("player" = ui.user, "count" = dice_count, "size" = dice_size, "results" = results, "mod" = modifier, "apply_to_all" = apply_to_all, "sum" = sum)) + return TRUE + /* Requires rust g dice + if("custom_roll") + var/input = params["custom"] + if(length(input) > 200) + to_chat(ui.user, span_warning("Your input was too long.")) + return FALSE + var/static/regex/valid_dice_roll = regex(@"^(\d+d\d+(\s*[\+\-\*\/]\s*\d+)?(\s*[\+\-\*\/]\s*\d+d\d+(\s*[\+\-\*\/]\s*\d+)?)*)$") + if(!findtext(input, valid_dice_roll)) + to_chat(ui.user, span_warning("Invalid input, please follow the common pattern e.g.: 2d6 + 3d10 + 5")) + return FALSE + var/result = rustg_roll_dice("[input]") + UNTYPED_LIST_ADD(last_rolls, list("player" = ui.user, "count" = input, "sum" = result)) + return TRUE + */ + if("clear_history") + last_rolls.Cut() + return TRUE + +/datum/board_game/rpg_dice/proc/check_crit(low, max, result) + if(result == low) + return 1 + if(result == max) + return 2 + return 0 diff --git a/code/modules/casino/board_games/space_battle.dm b/code/modules/casino/board_games/space_battle.dm new file mode 100644 index 0000000000..8e8158e3bf --- /dev/null +++ b/code/modules/casino/board_games/space_battle.dm @@ -0,0 +1,436 @@ +#define GAME_PLACE_SHIPS 1 +#define GAME_PLAYER_ONE 2 +#define GAME_PLAYER_TWO 3 +#define GAME_OVER 4 +#define GRID_SIZE 10 +#define PLAYER_ONE_PLACED_SHIPS 0x1 +#define PLAYER_TWO_PLACED_SHIPS 0x2 + +/obj/structure/casino_table/board_game/space_battle + name = GAME_SPACE_BATTLE + desc = "A game with the goal to destroy the opponent's ships." + icon_state = "gamble_space" + game_ui = /datum/board_game/space_battle + +/datum/board_game/space_battle + name = GAME_SPACE_BATTLE + table_icon = "gamble_space" + var/datum/weakref/player_one + var/datum/weakref/player_two + var/list/ship_count_pone = list() + var/list/ship_count_ptwo = list() + var/list/shots_fired_pone = list() + var/list/shots_fired_ptwo = list() + var/list/ships_placed_pone = list() + var/list/ships_placed_ptwo = list() + var/list/destroyed_ships_pone = list() + var/list/destroyed_ships_ptwo = list() + var/static/list/total_ships = list( + "Carrier" = 1, + "Cruiser" = 2, + "Corvette" = 3, + "Figher" = 4, + ) + var/static/list/ship_sizes = list( + "Carrier" = 6, + "Cruiser" = 4, + "Corvette" = 3, + "Figher" = 2, + ) + var/winner + var/ships_have_been_placed = NONE + +/datum/board_game/space_battle/Destroy(force) + player_one = null + player_two = null + . = ..() + +/datum/board_game/space_battle/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SpaceBattle", name) + ui.open() + +/datum/board_game/space_battle/tgui_static_data(mob/user) + return list( + "ship_sizes" = ship_sizes, + "total_ships" = total_ships + ) + +/datum/board_game/space_battle/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + + var/list/visible_ships = list() + if(ui.user == player_one_mob || game_state == GAME_OVER) + visible_ships += ships_placed_pone + if(ui.user == player_two_mob || game_state == GAME_OVER) + visible_ships += ships_placed_ptwo + + return list( + "current_player" = ui.user, + "player_one" = player_one_mob, + "player_two" = player_two_mob, + "all_placed" = ships_have_been_placed == (PLAYER_ONE_PLACED_SHIPS | PLAYER_TWO_PLACED_SHIPS), + "shots_fired_pone" = shots_fired_pone, + "shots_fired_ptwo" = shots_fired_ptwo, + "destroyed_ships_pone" = destroyed_ships_pone, + "destroyed_ships_ptwo" = destroyed_ships_ptwo, + "visible_ships" = visible_ships, + "ship_count_pone" = ship_count_pone, + "ship_count_ptwo" = ship_count_ptwo, + "game_state" = game_state, + "winner" = winner, + "has_won" = winner == ui.user.name + ) + +/datum/board_game/space_battle/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_player_one") + if(game_state != GAME_SETUP) + return FALSE + if(player_one?.resolve() == ui.user) + player_one = null + return TRUE + player_one = WEAKREF(ui.user) + return TRUE + if("be_player_two") + if(game_state != GAME_SETUP) + return FALSE + if(player_two?.resolve() == ui.user) + player_two = null + return TRUE + player_two = WEAKREF(ui.user) + return TRUE + if("swap_players") + if(game_state != GAME_SETUP) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + if("clear_game") + if(game_state == GAME_SETUP) + return FALSE + reset(TRUE) + return TRUE + if("prepare_game") + if(game_state != GAME_SETUP) + return FALSE + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + if(!player_one_mob || !player_two_mob) + return FALSE + game_state = GAME_PLACE_SHIPS + ship_count_pone = get_remaining_ships(1) + ship_count_ptwo = get_remaining_ships(2) + return TRUE + if("start_game") + if(game_state != GAME_PLACE_SHIPS) + return FALSE + if(!(ships_have_been_placed == (PLAYER_ONE_PLACED_SHIPS | PLAYER_TWO_PLACED_SHIPS))) + return FALSE + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + if(!player_one_mob || !player_two_mob) + return FALSE + ship_count_pone = get_alive_ships(1) + ship_count_ptwo = get_alive_ships(2) + game_state = GAME_PLAYER_ONE + return TRUE + if("play_again") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + return TRUE + if("play_again_swapped") + if(game_state < GAME_OVER) + return FALSE + if(!player_one?.resolve() || !player_two?.resolve()) + return FALSE + reset() + var/datum/weakref/temp_player = player_one + player_one = player_two + player_two = temp_player + return TRUE + if("place_ship") + if(game_state != GAME_PLACE_SHIPS) + return FALSE + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + if(!player_one_mob || !player_two_mob) + return FALSE + + var/list/ship_data = params["ship"] + if(!ship_data["name"] || !ship_data["coords"]) + return FALSE + + var/player = ship_data["player"] + if(!player) + return FALSE + + if(player == 1 && ui.user != player_one_mob) + return FALSE + if(player == 2 && ui.user != player_two_mob) + return FALSE + + var/allowed = total_ships[ship_data["name"]] + if(!allowed) + return FALSE + + var/list/current_ships + if(player == 1) + current_ships = ships_placed_pone + else + current_ships = ships_placed_ptwo + + var/count = 0 + for(var/list/existing in current_ships) + if(existing["name"] == ship_data["name"]) + count++ + + if(count >= allowed) + return FALSE + + var/list/new_coords = ship_data["coords"] + for(var/list/new_coord in new_coords) + if(!isnum(new_coord[1]) || !isnum(new_coord[2])) + return FALSE + if(!field_check(new_coord[1], new_coord[2])) + return FALSE + + for(var/list/existing_ship in current_ships) + for(var/list/coord in existing_ship["coords"]) + for(var/list/new_coord in new_coords) + if(coord[1] == new_coord[1] && coord[2] == new_coord[2]) + return FALSE + + UNTYPED_LIST_ADD(current_ships, ship_data) + if(player == 1) + ship_count_pone = get_remaining_ships(1) + else + ship_count_ptwo = get_remaining_ships(2) + + return TRUE + if("remove_ship") + if(game_state != GAME_PLACE_SHIPS) + return FALSE + var/mob/player_one_mob = player_one?.resolve() + var/mob/player_two_mob = player_two?.resolve() + if(!player_one_mob || !player_two_mob) + return FALSE + + var/player = params["player"] + if(player == 1 && ui.user != player_one_mob) + return FALSE + if(player == 2 && ui.user != player_two_mob) + return FALSE + + var/list/ships + if(player == 1) + ships = ships_placed_pone + else + ships = ships_placed_ptwo + + var/loc_x = params["loc_x"] + var/loc_y = params["loc_y"] + for(var/i in length(ships) to 1 step -1) + var/list/ship = ships[i] + for(var/list/coord in ship["coords"]) + if(coord[1] == loc_x && coord[2] == loc_y) + ships.Cut(i, i + 1) + if(player == 1) + ship_count_pone = get_remaining_ships(1) + else + ship_count_ptwo = get_remaining_ships(2) + return TRUE + return FALSE + if("game_action") + if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE) + if(params["data"]["player"] == 1) + return FALSE + if(player_actions(params["action"], params["data"], ui.user)) + if(game_state < GAME_OVER) + game_state = GAME_PLAYER_TWO + return TRUE + if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO) + if(params["data"]["player"] == 2) + return FALSE + if(player_actions(params["action"], params["data"], ui.user)) + if(game_state < GAME_OVER) + game_state = GAME_PLAYER_ONE + return TRUE + return FALSE + +/datum/board_game/space_battle/proc/reset(full) + ship_count_pone.Cut() + ship_count_ptwo.Cut() + shots_fired_pone.Cut() + shots_fired_ptwo.Cut() + ships_placed_pone.Cut() + ships_placed_ptwo.Cut() + destroyed_ships_pone.Cut() + destroyed_ships_ptwo.Cut() + winner = null + ships_have_been_placed = NONE + if(full) + game_state = GAME_SETUP + player_one = null + player_two = null + else + game_state = GAME_PLACE_SHIPS + +/datum/board_game/space_battle/proc/player_actions(action, list/params, mob/user) + switch(action) + if("fire_shot") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + + var/key = validated_data[1] + + var/list/opponent_ships + var/list/current_shots + + if(game_state == GAME_PLAYER_ONE) + current_shots = shots_fired_pone + opponent_ships = ships_placed_ptwo + else + current_shots = shots_fired_ptwo + opponent_ships = ships_placed_pone + + if(!isnull(current_shots[key])) + return FALSE + + var/hit = 0 + for(var/list/ship in opponent_ships) + for(var/coord in ship["coords"]) + if(coord[1] == validated_data[2] && coord[2] == validated_data[3]) + hit = 1 + break + if(hit) + break + + current_shots[key] = hit + + if(hit) + validate_victory(opponent_ships, current_shots, user) + + return TRUE + +/datum/board_game/space_battle/proc/validate_victory(list/opponent_ships, list/current_shots, mob/user) + for(var/list/ship in opponent_ships) + var/ship_destroyed = TRUE + for(var/coord in ship["coords"]) + var/key = "[coord[1]],[coord[2]]" + if(!current_shots[key]) + ship_destroyed = FALSE + break + + if(ship_destroyed) + if(game_state == GAME_PLAYER_ONE) + if(!(destroyed_ships_pone.Find(ship))) + UNTYPED_LIST_ADD(destroyed_ships_pone, ship) + ship_count_pone = get_alive_ships(1) + else + if(!(destroyed_ships_ptwo.Find(ship))) + UNTYPED_LIST_ADD(destroyed_ships_ptwo, ship) + ship_count_ptwo = get_alive_ships(2) + + for(var/list/ship in opponent_ships) + var/key + for(var/coord in ship["coords"]) + key = "[coord[1]],[coord[2]]" + if(!current_shots[key]) + return + + winner = user.name + game_state = GAME_OVER + +/datum/board_game/space_battle/proc/field_check(new_x_location, new_y_location) + if(new_x_location < 1 || new_x_location > GRID_SIZE) + return FALSE + if(new_y_location < 1 || new_y_location > GRID_SIZE) + return FALSE + return TRUE + +/datum/board_game/space_battle/proc/validate_coords(list/params) + var/x_loc = text2num(params["loc_x"]) + var/y_loc = text2num(params["loc_y"]) + + if(!isnum(x_loc) || !isnum(y_loc)) + return null + + if(!field_check(x_loc, y_loc)) + return null + + var/key = "[x_loc],[y_loc]" + return list(key, x_loc, y_loc) + +/datum/board_game/space_battle/proc/get_remaining_ships(player) + var/list/remaining_ships = list() + var/list/placed_ships + var/ship_placed_flag + + if(player == 1) + placed_ships = ships_placed_pone + ship_placed_flag = PLAYER_ONE_PLACED_SHIPS + else if(player == 2) + placed_ships = ships_placed_ptwo + ship_placed_flag = PLAYER_TWO_PLACED_SHIPS + else + return remaining_ships + + for(var/ship_name in ship_sizes) + var/allowed_count = total_ships[ship_name] + var/placed_count = 0 + + for(var/list/placed_ship in placed_ships) + if(placed_ship["name"] == ship_name) + placed_count++ + + var/remaining_count = allowed_count - placed_count + if(remaining_count > 0) + remaining_ships[ship_name] = remaining_count + + if(length(remaining_ships)) + ships_have_been_placed &= ~ship_placed_flag + else + ships_have_been_placed |= ship_placed_flag + + return remaining_ships + +/datum/board_game/space_battle/proc/get_alive_ships(player) + var/list/alive_ships = list() + var/list/ships + + if(player == 1) + ships = ships_placed_pone + else if(player == 2) + ships = ships_placed_ptwo + else + return alive_ships + + for(var/list/ship in ships) + if(game_state == GAME_PLAYER_ONE && !(destroyed_ships_pone.Find(ship))) + alive_ships[ship["name"]] = length(alive_ships) ? alive_ships[ship["name"]] + 1 : 1 + + if(game_state == GAME_PLAYER_TWO && !(destroyed_ships_ptwo.Find(ship))) + alive_ships[ship["name"]] = length(alive_ships) ? alive_ships[ship["name"]] + 1 : 1 + + return alive_ships + +#undef GAME_PLACE_SHIPS +#undef GAME_PLAYER_ONE +#undef GAME_PLAYER_TWO +#undef GAME_OVER +#undef GRID_SIZE +#undef PLAYER_ONE_PLACED_SHIPS +#undef PLAYER_TWO_PLACED_SHIPS diff --git a/code/modules/casino/board_games/tic_tac_toe.dm b/code/modules/casino/board_games/tic_tac_toe.dm new file mode 100644 index 0000000000..d334949730 --- /dev/null +++ b/code/modules/casino/board_games/tic_tac_toe.dm @@ -0,0 +1,51 @@ +#define GAME_PLAYER_ONE 1 +#define GRID_SIZE 3 + +/obj/structure/casino_table/board_game/tic_tac_toe + name = GAME_TIC_TAC_TOE + desc = "A small tic-tac-toe board." + icon_state = "gamble_toe" + game_ui = /datum/board_game/four_row/tic_tac_toe + +/datum/board_game/four_row/tic_tac_toe + name = GAME_TIC_TAC_TOE + table_icon = "gamble_toe" + grid_x_size = GRID_SIZE + grid_y_size = GRID_SIZE + win_count = GRID_SIZE + +/datum/board_game/four_row/tic_tac_toe/tgui_static_data(mob/user) + return list( + "colors" = possible_colors - "blue" + ) + +/datum/board_game/four_row/tic_tac_toe/player_actions(action, list/params, mob/user) + switch(action) + if("place_chip") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + + var/key = validated_data[1] + if(placed_chips_pone[key] || placed_chips_ptwo[key]) + return FALSE + + var/x_loc = validated_data[2] + var/y_loc = validated_data[3] + + if(game_state == GAME_PLAYER_ONE) + placed_chips_pone[key] = TRUE + else + placed_chips_ptwo[key] = TRUE + + validate_victory(x_loc, y_loc, user.name) + return TRUE + +/datum/board_game/four_row/tic_tac_toe/set_new_size(new_size) + return FALSE + +/datum/board_game/four_row/change_win_count(new_count) + return FALSE + +#undef GAME_PLAYER_ONE +#undef GRID_SIZE diff --git a/code/modules/casino/board_games/vore_sweeper.dm b/code/modules/casino/board_games/vore_sweeper.dm new file mode 100644 index 0000000000..1cfcf669f1 --- /dev/null +++ b/code/modules/casino/board_games/vore_sweeper.dm @@ -0,0 +1,321 @@ +#define GAME_PLAYING 1 +#define GAME_LOST 2 +#define GAME_WON 3 +#define MAX_MINE_RATE 0.8 + +/obj/structure/casino_table/board_game/vore_sweeper + name = GAME_SWEEPER + desc = "A game about avoiding the mines" + icon_state = "gamble_sweeper" + game_ui = /datum/board_game/vore_sweeper + +/datum/board_game/vore_sweeper + name = GAME_SWEEPER + table_icon = "gamble_sweeper" + var/grid_size = 8 + var/mine_count = 10 + var/datum/weakref/dealer + var/list/placed_mines = list() + var/list/revealed_fields = list() + var/list/placed_flags = list() + +/datum/board_game/vore_sweeper/New(atom/holder) + . = ..() + parent = holder + +/datum/board_game/vore_sweeper/Destroy(force) + dealer = null + parent = null + . = ..() + +/datum/board_game/vore_sweeper/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "VoreSweeper", name) + ui.open() + +/datum/board_game/vore_sweeper/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/mob/dealer_mob = dealer?.resolve() + + var/placed_mine_data = game_state > GAME_PLAYING || (ui.user == dealer_mob) ? placed_mines : null + var/total_tiles = grid_size * grid_size + return list( + "grid_size" = grid_size, + "mine_count" = mine_count, + "max_mines" = round(total_tiles * MAX_MINE_RATE), + "dealer" = dealer_mob, + "placed_mines" = placed_mine_data, + "revealed_fields" = revealed_fields, + "placed_flags" = placed_flags, + "game_state" = game_state, + "is_dealer" = dealer_mob == ui.user + ) + +/datum/board_game/vore_sweeper/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if("be_dealer") + if(game_state == GAME_PLAYING) + return FALSE + dealer = WEAKREF(ui.user) + return TRUE + if("clear_dealer") + var/mob/dealer_mob = dealer?.resolve() + if(!dealer_mob) + return FALSE + if(dealer_mob == ui.user) + parent.atom_say("[ui.user] stopped dealing.") + dealer = null + return TRUE + if(get_dist(ui.user, dealer_mob) > 3) + parent.atom_say("Dealer has been cleared by [ui.user].") + dealer = null + return TRUE + return FALSE + if("restart_game") + var/mob/dealer_mob = dealer?.resolve() + if(game_state < GAME_PLAYING) + return FALSE + placed_mines.Cut() + revealed_fields.Cut() + placed_flags.Cut() + if(!dealer_mob && game_state > GAME_PLAYING) + auto_place_mines(ui.user, TRUE) + return TRUE + game_state = GAME_SETUP + return TRUE + if("game_action") + if(player_actions(params["action"], params["data"], ui.user)) + return TRUE + return FALSE + if("setup_action") + if(dealer_actions(params["action"], params["data"], ui.user)) + return TRUE + return FALSE + +/datum/board_game/vore_sweeper/proc/player_actions(action, list/params, mob/user) + if(user == dealer?.resolve()) + return FALSE + if(game_state != GAME_PLAYING) + return FALSE + switch(action) + if("open_field") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + var/key = validated_data[1] + if(placed_flags[key]) + return FALSE + if(revealed_fields[key]) + return FALSE + if(placed_mines[key]) + game_state = GAME_LOST + revealed_fields[key] = "M" + return TRUE + var/mine_count = count_surrounding_mines(validated_data[2], validated_data[3]) + revealed_fields[key] = mine_count + if(!mine_count) + reveal_empty_area(validated_data[2], validated_data[3]) + validate_victory() + return TRUE + if("toggle_flag") + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + var/key = validated_data[1] + if(revealed_fields[key]) + return FALSE + if(placed_flags[key]) + placed_flags -= key + return TRUE + placed_flags[key] = TRUE + validate_flag_victory() + return TRUE + +/datum/board_game/vore_sweeper/proc/validate_victory() + var/total_tiles = grid_size * grid_size + var/safe_tiles = total_tiles - length(placed_mines) + if(length(revealed_fields) >= safe_tiles) + game_state = GAME_WON + +/datum/board_game/vore_sweeper/proc/validate_flag_victory() + var/all_flagged = TRUE + + for(var/mine_key in placed_mines) + if(placed_mines[mine_key] && !placed_flags[mine_key]) + all_flagged = FALSE + break + + for(var/flag_key in placed_flags) + if(!placed_mines[flag_key]) + all_flagged = FALSE + break + + if(!all_flagged) + return + + for(var/x = 1 to grid_size) + for(var/y = 1 to grid_size) + var/key = "[x],[y]" + + if(placed_mines[key]) + continue + + revealed_fields[key] = count_surrounding_mines(x, y) + + game_state = GAME_WON + +/datum/board_game/vore_sweeper/proc/dealer_actions(action, list/params, mob/user) + if(user != dealer?.resolve()) + return FALSE + if(game_state != GAME_SETUP) + return FALSE + switch(action) + if("change_grid_size") + var/new_grid_size = text2num(params["new_grid"]) + if(!new_grid_size) + return FALSE + if(new_grid_size < 4 || new_grid_size > 16) + return FALSE + validate_mine_count(mine_count, new_grid_size) + grid_size = new_grid_size + return TRUE + if("change_mine_count") + var/new_mine_count = text2num(params["new_mines"]) + if(!new_mine_count) + return FALSE + validate_mine_count(new_mine_count, grid_size) + return TRUE + if("place_mine") + if(length(placed_mines) >= mine_count) + return FALSE + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + var/key = validated_data[1] + if(placed_mines[key]) + return FALSE + placed_mines[key] = TRUE + return TRUE + if("remove_mine") + if(game_state != GAME_SETUP) + return FALSE + if(length(placed_mines) <= 0) + return FALSE + var/list/validated_data = validate_coords(params) + if(!validated_data) + return FALSE + var/key = validated_data[1] + placed_mines -= key + return TRUE + if("auto_place_mines") + return auto_place_mines(user) + if("auto_place_mines_self") + return auto_place_mines(user, TRUE) + if("clear_all_mines") + placed_mines.Cut() + return TRUE + if("start_game") + game_state = GAME_PLAYING + return TRUE + +/datum/board_game/vore_sweeper/proc/reveal_empty_area(x, y) + var/list/to_check = list(list(x, y)) + var/list/checked = list() + var/i = 1 + + while(i <= length(to_check)) + var/current = to_check[i] + i += 1 + var/cx = current[1] + var/cy = current[2] + var/key = "[cx],[cy]" + + if(checked[key]) + continue + checked[key] = TRUE + + if(revealed_fields[key] || placed_flags[key]) + continue + + var/adjacent_mines = count_surrounding_mines(cx, cy) + revealed_fields[key] = adjacent_mines + + if(adjacent_mines == 0) + for(var/dx = -1 to 1) + for(var/dy = -1 to 1) + if(dx == 0 && dy == 0) + continue + var/nx = cx + dx + var/ny = cy + dy + if(field_check(nx, ny)) + to_check += list(list(nx, ny)) + +/datum/board_game/vore_sweeper/proc/auto_place_mines(mob/user, play) + var/placed = length(placed_mines) + if(placed && play) + to_chat(user, span_warning("You can't do this while there are mines placed.")) + while(placed < mine_count) + var/x = rand(1, grid_size) + var/y = rand(1, grid_size) + var/key = "[x],[y]" + if(!placed_mines[key]) + placed_mines[key] = TRUE + placed++ + if(play) + dealer = null + game_state = GAME_PLAYING + return TRUE + +/datum/board_game/vore_sweeper/proc/field_check(new_x_location, new_y_location) + if(new_x_location < 1 || new_x_location > grid_size) + return FALSE + if(new_y_location < 1 || new_y_location > grid_size) + return FALSE + return TRUE + +/datum/board_game/vore_sweeper/proc/count_surrounding_mines(x, y) + var/count = 0 + + for(var/dx = -1 to 1) + for(var/dy = -1 to 1) + if(dx == 0 && dy == 0) + continue + + var/check_x = x + dx + var/check_y = y + dy + + if(placed_mines["[check_x],[check_y]"]) + count++ + + return count + +/datum/board_game/vore_sweeper/proc/validate_mine_count(mines, size) + var/total_tiles = size * size + var/max_mines = round(total_tiles * MAX_MINE_RATE) + if(mines <= max_mines) + mine_count = mines + return + parent.atom_say("The grid with [total_tiles] tiles only supports a maximum of [max_mines] mines.") + mine_count = max_mines + +/datum/board_game/vore_sweeper/proc/validate_coords(list/params) + var/x_loc = text2num(params["loc_x"]) + var/y_loc = text2num(params["loc_y"]) + + if(!isnum(x_loc) || !isnum(y_loc)) + return null + + if(!field_check(x_loc, y_loc)) + return null + + var/key = "[x_loc],[y_loc]" + return list(key, x_loc, y_loc) + +#undef GAME_PLAYING +#undef GAME_LOST +#undef GAME_WON +#undef MAX_MINE_RATE diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index dca2880b2e..1432c43f42 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -767,19 +767,26 @@ var/global/image/backplane return TRUE -/atom/proc/living_mobs_in_view(var/range = world.view, var/count_held = FALSE) +/atom/proc/living_mobs_in_view(var/range = world.view, var/count_held = FALSE, var/needs_client = FALSE) var/list/viewers = oviewers(src, range) if(count_held) viewers = viewers(src,range) var/list/living = list() - for(var/mob/living/L in viewers) - if(L.is_incorporeal()) + for(var/mob/living/living_in_view in viewers) + if(living_in_view.is_incorporeal()) continue - living += L - if(count_held) - for(var/obj/item/holder/H in L.contents) - if(istype(H.held_mob, /mob/living)) - living += H.held_mob + if(needs_client && !living_in_view.client) + continue + living += living_in_view + if(!count_held) + continue + for(var/obj/item/holder/mob_holder in living_in_view.contents) + if(!isliving(mob_holder.held_mob)) + continue + var/mob/living/held_living = mob_holder.held_mob + if(needs_client && !held_living.client) + continue + living += held_living return living /proc/censor_swears(t) diff --git a/code/modules/pda/app.dm b/code/modules/pda/app.dm index 5b9e942d6b..43ad146cab 100644 --- a/code/modules/pda/app.dm +++ b/code/modules/pda/app.dm @@ -72,7 +72,7 @@ pda.current_app = src return 1 -/datum/data/pda/app/proc/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/proc/update_ui(mob/user, list/data) // Utilities just have a button on the home screen, but custom code when clicked @@ -104,6 +104,6 @@ pda.update_shortcuts() return 1 -/datum/data/pda/utility/scanmode/proc/scan_mob(mob/living/C as mob, mob/living/user as mob) +/datum/data/pda/utility/scanmode/proc/scan_mob(mob/living/C, mob/living/user) -/datum/data/pda/utility/scanmode/proc/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob) +/datum/data/pda/utility/scanmode/proc/scan_atom(atom/A, mob/user) diff --git a/code/modules/pda/cart_apps.dm b/code/modules/pda/cart_apps.dm index 1d6da01edb..a3853fba8a 100644 --- a/code/modules/pda/cart_apps.dm +++ b/code/modules/pda/cart_apps.dm @@ -7,7 +7,7 @@ var/message1 // used for status_displays var/message2 -/datum/data/pda/app/status_display/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/status_display/update_ui(mob/user, list/data) data["records"] = list( "message1" = message1 ? message1 : "(none)", "message2" = message2 ? message2 : "(none)") @@ -63,7 +63,7 @@ template = "pda_signaller" category = "Utilities" -/datum/data/pda/app/signaller/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/signaller/update_ui(mob/user, list/data) if(pda.cartridge && istype(pda.cartridge.radio, /obj/item/radio/integrated/signal)) var/obj/item/radio/integrated/signal/R = pda.cartridge.radio data["frequency"] = R.frequency @@ -112,7 +112,7 @@ QDEL_NULL(power_monitor) return ..() -/datum/data/pda/app/power/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/power/update_ui(mob/user, list/data) data.Add(power_monitor.tgui_data(user)) /datum/data/pda/app/power/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) @@ -128,7 +128,7 @@ /datum/data/pda/app/crew_records var/datum/data/record/general_records = null -/datum/data/pda/app/crew_records/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/crew_records/update_ui(mob/user, list/data) var/list/records[0] if(general_records && (general_records in GLOB.data_core.general)) @@ -169,7 +169,7 @@ var/datum/data/record/medical_records = null -/datum/data/pda/app/crew_records/medical/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/crew_records/medical/update_ui(mob/user, list/data) var/list/records = ..() if(!records) return @@ -194,7 +194,7 @@ var/datum/data/record/security_records = null -/datum/data/pda/app/crew_records/security/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/crew_records/security/update_ui(mob/user, list/data) var/list/records = ..() if(!records) return @@ -217,7 +217,7 @@ template = "pda_supply" category = "Quartermaster" -/datum/data/pda/app/supply/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/supply/update_ui(mob/user, list/data) var/supplyData[0] var/datum/shuttle/autodock/ferry/supply/shuttle = SSsupply.shuttle if (shuttle) @@ -254,7 +254,7 @@ template = "pda_janitor" category = "Utilities" -/datum/data/pda/app/janitor/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/janitor/update_ui(mob/user, list/data) var/JaniData[0] var/turf/cl = get_turf(pda) diff --git a/code/modules/pda/cart_vr.dm b/code/modules/pda/cart_vr.dm index 6c39989d20..efdc73b703 100644 --- a/code/modules/pda/cart_vr.dm +++ b/code/modules/pda/cart_vr.dm @@ -32,15 +32,15 @@ hold.max_storage_space = slots * 2 hold.max_w_class = ITEMSIZE_SMALL -/obj/item/cartridge/storage/attack_hand(mob/user as mob) +/obj/item/cartridge/storage/attack_hand(mob/user) if (hold.handle_attack_hand(user)) //otherwise interact as a regular storage item ..(user) -/obj/item/cartridge/storage/attackby(obj/item/W as obj, mob/user as mob) +/obj/item/cartridge/storage/attackby(obj/item/W, mob/user) ..() return hold.attackby(W, user) -/obj/item/cartridge/storage/MouseDrop(obj/over_object as obj) +/obj/item/cartridge/storage/MouseDrop(obj/over_object) if (hold.handle_mousedrop(usr, over_object)) ..(over_object) diff --git a/code/modules/pda/core_apps.dm b/code/modules/pda/core_apps.dm index 46a3aa9e5d..22426ba9ed 100644 --- a/code/modules/pda/core_apps.dm +++ b/code/modules/pda/core_apps.dm @@ -3,7 +3,7 @@ template = "pda_main_menu" hidden = 1 -/datum/data/pda/app/main_menu/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/main_menu/update_ui(mob/user, list/data) title = pda.name data["app"]["is_home"] = 1 @@ -63,7 +63,7 @@ note = "Thank you for choosing the [pda.model_name]!" notetitle = "Congratulations!" -/datum/data/pda/app/notekeeper/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/notekeeper/update_ui(mob/user, list/data) data["note"] = note // current pda notes data["notename"] = "Note [GLOB.alphabet_upper[currentnote]] : [notetitle]" @@ -206,7 +206,7 @@ icon = "user" template = "pda_manifest" -/datum/data/pda/app/manifest/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/manifest/update_ui(mob/user, list/data) if(GLOB.data_core) GLOB.data_core.get_manifest_list() data["manifest"] = GLOB.PDA_Manifest @@ -221,7 +221,7 @@ template = "pda_atmos_scan" category = "Utilities" -/datum/data/pda/app/atmos_scanner/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/atmos_scanner/update_ui(mob/user, list/data) data["aircontents"] = get_gas_mixture_default_scan_data(get_turf(user)) /datum/data/pda/app/news @@ -231,7 +231,7 @@ var/newsfeed_channel -/datum/data/pda/app/news/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/news/update_ui(mob/user, list/data) data["feeds"] = compile_news() data["latest_news"] = get_recent_news() if(newsfeed_channel) diff --git a/code/modules/pda/game_launcher.dm b/code/modules/pda/game_launcher.dm new file mode 100644 index 0000000000..1b866bf859 --- /dev/null +++ b/code/modules/pda/game_launcher.dm @@ -0,0 +1,125 @@ +/datum/data/pda/app/game_launcher + name = "Game Launcher" + icon = "dice" + notify_icon = "dice-d20" + title = "Game Launcher V1.0" + template = "pda_game_launcher" + + var/datum/board_game/vore_sweeper/voresweeper + var/datum/board_game/four_row/fourrow + var/datum/board_game/space_battle/spacebattle + var/datum/board_game/rpg_dice/rpgdice + var/datum/board_game/chess/chess + var/datum/board_game/checkers/checkers + var/datum/board_game/nine_mens/ninemens + var/datum/board_game/four_row/tic_tac_toe/tictactoe + +/datum/data/pda/app/game_launcher/update_ui(mob/user, list/data) + data["available_games"] = list(GAME_SWEEPER = voresweeper, GAME_FOUR_ROW = fourrow, GAME_SPACE_BATTLE = spacebattle, GAME_RGP_DICE = rpgdice, GAME_CHESS = chess, GAME_CHECKERS = checkers, GAME_NINE_MENS_MORRIS = ninemens, GAME_TIC_TAC_TOE = tictactoe) + +/datum/data/pda/app/game_launcher/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + + switch(action) + if(GAME_SWEEPER) + if(params["close"]) + if(!voresweeper) + return FALSE + QDEL_NULL(voresweeper) + return TRUE + if(!voresweeper) + voresweeper = new(pda) + voresweeper.tgui_interact(ui.user) + return TRUE + if(GAME_FOUR_ROW) + if(params["close"]) + if(!fourrow) + return FALSE + QDEL_NULL(fourrow) + return TRUE + if(!fourrow) + fourrow = new(pda) + fourrow.tgui_interact(ui.user) + return TRUE + if(GAME_SPACE_BATTLE) + if(params["close"]) + if(!spacebattle) + return FALSE + QDEL_NULL(spacebattle) + return TRUE + if(!spacebattle) + spacebattle = new(pda) + spacebattle.tgui_interact(ui.user) + return TRUE + if(GAME_RGP_DICE) + if(params["close"]) + if(!rpgdice) + return FALSE + QDEL_NULL(rpgdice) + return TRUE + if(!rpgdice) + rpgdice = new(pda) + rpgdice.tgui_interact(ui.user) + return TRUE + if(GAME_CHESS) + if(params["close"]) + if(!chess) + return FALSE + QDEL_NULL(chess) + return TRUE + if(!chess) + chess = new(pda) + chess.tgui_interact(ui.user) + return TRUE + if(GAME_CHECKERS) + if(params["close"]) + if(!checkers) + return FALSE + QDEL_NULL(checkers) + return TRUE + if(!checkers) + checkers = new(pda) + checkers.tgui_interact(ui.user) + return TRUE + if(GAME_NINE_MENS_MORRIS) + if(params["close"]) + if(!ninemens) + return FALSE + QDEL_NULL(ninemens) + return TRUE + if(!ninemens) + ninemens = new(pda) + ninemens.tgui_interact(ui.user) + return TRUE + if(GAME_TIC_TAC_TOE) + if(params["close"]) + if(!tictactoe) + return FALSE + QDEL_NULL(tictactoe) + return TRUE + if(!tictactoe) + tictactoe = new(pda) + tictactoe.tgui_interact(ui.user) + return TRUE + return TRUE + +/datum/data/pda/app/game_launcher/Destroy() + if(voresweeper) + QDEL_NULL(voresweeper) + if(fourrow) + QDEL_NULL(fourrow) + if(spacebattle) + QDEL_NULL(spacebattle) + if(rpgdice) + QDEL_NULL(rpgdice) + if(chess) + QDEL_NULL(chess) + if(checkers) + QDEL_NULL(checkers) + if(checkers) + QDEL_NULL(ninemens) + if(tictactoe) + QDEL_NULL(tictactoe) + . = ..() diff --git a/code/modules/pda/messenger.dm b/code/modules/pda/messenger.dm index d00c7dfaa6..f3669c5483 100644 --- a/code/modules/pda/messenger.dm +++ b/code/modules/pda/messenger.dm @@ -18,7 +18,7 @@ . = ..() unnotify() -/datum/data/pda/app/messenger/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/messenger/update_ui(mob/user, list/data) data["silent"] = notify_silent // does the pda make noise when it receives a message? data["toff"] = toff // is the messenger function turned off? data["active_conversation"] = active_conversation // Which conversation are we following right now? diff --git a/code/modules/pda/messenger_plugins.dm b/code/modules/pda/messenger_plugins.dm index 11f261eeac..eb62d04989 100644 --- a/code/modules/pda/messenger_plugins.dm +++ b/code/modules/pda/messenger_plugins.dm @@ -1,13 +1,13 @@ /datum/data/pda/messenger_plugin var/datum/data/pda/app/messenger/messenger -/datum/data/pda/messenger_plugin/proc/user_act(mob/user as mob, obj/item/pda/P) +/datum/data/pda/messenger_plugin/proc/user_act(mob/user, obj/item/pda/P) /datum/data/pda/messenger_plugin/virus name = "*Send Virus*" -/datum/data/pda/messenger_plugin/virus/user_act(mob/user as mob, obj/item/pda/P) +/datum/data/pda/messenger_plugin/virus/user_act(mob/user, obj/item/pda/P) var/datum/data/pda/app/messenger/M = P.find_program(/datum/data/pda/app/messenger) if(M && !M.toff && pda.cartridge.charges > 0) @@ -19,7 +19,7 @@ /datum/data/pda/messenger_plugin/virus/clown icon = "star" -/datum/data/pda/messenger_plugin/virus/clown/user_act(mob/user as mob, obj/item/pda/P) +/datum/data/pda/messenger_plugin/virus/clown/user_act(mob/user, obj/item/pda/P) . = ..(user, P) if(.) user.show_message(span_notice("Virus sent!"), 1) @@ -30,7 +30,7 @@ /datum/data/pda/messenger_plugin/virus/mime icon = "arrow-circle-down" -/datum/data/pda/messenger_plugin/virus/mime/user_act(mob/user as mob, obj/item/pda/P) +/datum/data/pda/messenger_plugin/virus/mime/user_act(mob/user, obj/item/pda/P) . = ..(user, P) if(.) user.show_message(span_notice("Virus sent!"), 1) @@ -44,7 +44,7 @@ name = "*Detonate*" icon = "exclamation-circle" -/datum/data/pda/messenger_plugin/virus/detonate/user_act(mob/user as mob, obj/item/pda/P) +/datum/data/pda/messenger_plugin/virus/detonate/user_act(mob/user, obj/item/pda/P) . = ..(user, P) if(.) var/difficulty = 0 diff --git a/code/modules/pda/nerdle.dm b/code/modules/pda/nerdle.dm index 2cf975bb10..a6cc79d373 100644 --- a/code/modules/pda/nerdle.dm +++ b/code/modules/pda/nerdle.dm @@ -89,7 +89,7 @@ LAZYADD(serialized_guesses, list(out)) // Wrap it in a list so it stays a list -/datum/data/pda/app/nerdle/update_ui(mob/user as mob, list/data) +/datum/data/pda/app/nerdle/update_ui(mob/user, list/data) data["guesses"] = serialized_guesses data["guesses_raw"] = guesses data["max"] = max_guesses diff --git a/code/modules/pda/pda.dm b/code/modules/pda/pda.dm index 1aae978d2f..f08086e941 100644 --- a/code/modules/pda/pda.dm +++ b/code/modules/pda/pda.dm @@ -47,6 +47,7 @@ new/datum/data/pda/app/manifest, new/datum/data/pda/app/atmos_scanner, new/datum/data/pda/app/nerdle, + new/datum/data/pda/app/game_launcher, new/datum/data/pda/utility/scanmode/notes, new/datum/data/pda/utility/flashlight) var/list/shortcut_cache = list() @@ -177,7 +178,7 @@ /obj/item/pda/GetID() return id -/obj/item/pda/MouseDrop(obj/over_object as obj, src_location, over_location) +/obj/item/pda/MouseDrop(obj/over_object, src_location, over_location) var/mob/M = usr if((!istype(over_object, /atom/movable/screen)) && can_use(usr)) return attack_self(M) @@ -389,7 +390,7 @@ start_program(find_program(/datum/data/pda/app/main_menu)) -/obj/item/pda/proc/id_check(mob/user as mob, choice as num)//To check for IDs; 1 for in-pda use, 2 for out of pda use. +/obj/item/pda/proc/id_check(mob/user, choice)//To check for IDs; 1 for in-pda use, 2 for out of pda use. if(choice == 1) if (id) remove_id() @@ -411,7 +412,7 @@ return 0 // access to status display signals -/obj/item/pda/attackby(obj/item/C as obj, mob/user) +/obj/item/pda/attackby(obj/item/C, mob/user) ..() if(istype(C, /obj/item/cartridge) && !cartridge) cartridge = C @@ -456,11 +457,11 @@ add_overlay("pda-pen") return -/obj/item/pda/attack(mob/living/C as mob, mob/living/user as mob) +/obj/item/pda/attack(mob/living/C, mob/living/user) if (istype(C, /mob/living/carbon) && scanmode) scanmode.scan_mob(C, user) -/obj/item/pda/afterattack(atom/A as mob|obj|turf|area, mob/user as mob, proximity) +/obj/item/pda/afterattack(atom/A, mob/user, proximity) if(proximity && scanmode) scanmode.scan_atom(A, user) diff --git a/code/modules/pda/pda_subtypes.dm b/code/modules/pda/pda_subtypes.dm index d47798e644..7c62098f06 100644 --- a/code/modules/pda/pda_subtypes.dm +++ b/code/modules/pda/pda_subtypes.dm @@ -213,7 +213,7 @@ name = "Civilian Services Department (Relay)" cartridges_to_send_to = GLOB.civilian_cartridges -/obj/item/pda/clown/Crossed(atom/movable/AM as mob|obj) //Clown PDA is slippery. +/obj/item/pda/clown/Crossed(atom/movable/AM) //Clown PDA is slippery. if(AM.is_incorporeal()) return if (isliving(AM)) diff --git a/code/modules/pda/utilities.dm b/code/modules/pda/utilities.dm index 8ab4d602e8..986649de68 100644 --- a/code/modules/pda/utilities.dm +++ b/code/modules/pda/utilities.dm @@ -44,7 +44,7 @@ base_name = "Med Scanner" icon = "heart-o" -/datum/data/pda/utility/scanmode/medical/scan_mob(mob/living/C as mob, mob/living/user as mob) +/datum/data/pda/utility/scanmode/medical/scan_mob(mob/living/C, mob/living/user) C.visible_message(span_warning("[user] has analyzed [C]'s vitals!")) user.show_message(span_notice("Analyzing Results for [C]:")) if(C.status_flags & FAKEDEATH) @@ -79,7 +79,7 @@ base_name = "DNA Scanner" icon = "link" -/datum/data/pda/utility/scanmode/dna/scan_mob(mob/living/C as mob, mob/living/user as mob) +/datum/data/pda/utility/scanmode/dna/scan_mob(mob/living/C, mob/living/user) if(ishuman(C)) var/mob/living/carbon/human/H = C if(!istype(H.dna, /datum/dna)) @@ -88,7 +88,7 @@ to_chat(user, span_notice("[H]'s Fingerprints: [md5(H.dna.uni_identity)]")) scan_blood(C, user) -/datum/data/pda/utility/scanmode/dna/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob) +/datum/data/pda/utility/scanmode/dna/scan_atom(atom/A, mob/user) scan_blood(A, user) /datum/data/pda/utility/scanmode/dna/proc/scan_blood(atom/A, mob/user) @@ -105,7 +105,7 @@ base_name = "Halogen Counter" icon = "exclamation-circle" -/datum/data/pda/utility/scanmode/halogen/scan_mob(mob/living/C as mob, mob/living/user as mob) +/datum/data/pda/utility/scanmode/halogen/scan_mob(mob/living/C, mob/living/user) C.visible_message(span_warning("[user] has analyzed [C]'s radiation levels!")) user.show_message(span_notice("Analyzing Results for [C]:")) @@ -118,7 +118,7 @@ base_name = "Reagent Scanner" icon = "flask" -/datum/data/pda/utility/scanmode/reagent/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob) +/datum/data/pda/utility/scanmode/reagent/scan_atom(atom/A, mob/user) if(!isnull(A.reagents)) if(A.reagents.reagent_list.len > 0) var/reagents_length = A.reagents.reagent_list.len @@ -134,7 +134,7 @@ base_name = "Gas Scanner" icon = "tachometer-alt" -/datum/data/pda/utility/scanmode/gas/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob) +/datum/data/pda/utility/scanmode/gas/scan_atom(atom/A, mob/user) pda.analyze_gases(A, user) /datum/data/pda/utility/scanmode/notes @@ -146,7 +146,7 @@ . = ..() notes = pda.find_program(/datum/data/pda/app/notekeeper) -/datum/data/pda/utility/scanmode/notes/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob) +/datum/data/pda/utility/scanmode/notes/scan_atom(atom/A, mob/user) if(notes && istype(A, /obj/item/paper)) var/obj/item/paper/P = A var/list/brlist = list("p", "/p", "br", "hr", "h1", "h2", "h3", "h4", "/h1", "/h2", "/h3", "/h4") diff --git a/code/modules/tgui/states/board_game.dm b/code/modules/tgui/states/board_game.dm new file mode 100644 index 0000000000..7ced0b7ef0 --- /dev/null +++ b/code/modules/tgui/states/board_game.dm @@ -0,0 +1,26 @@ +/** + * tgui state: board_game_state + * + * Checks that the default living handling or if the user or the object are inside a belly + **/ + +GLOBAL_DATUM_INIT(tgui_board_game_state, /datum/tgui_state/board_game_state, new) + +/datum/tgui_state/board_game_state/can_use_topic(atom/src_object, mob/user) + if(!isliving(user)) + return STATUS_UPDATE + var/mob/living/living_user = user + if(isbelly(living_user.loc) || isbelly(src_object.loc)) + return living_user.board_game_can_use_tgui_topic(src_object) + return living_user.default_can_use_tgui_topic(src_object) + +/mob/proc/board_game_can_use_tgui_topic(atom/src_object) + return STATUS_CLOSE + +/mob/living/board_game_can_use_tgui_topic(atom/src_object) + if(is_incorporeal()) + return STATUS_CLOSE + var/dist = get_dist(get_turf(src_object), get_turf(src)) + if(dist <= 1) + return STATUS_INTERACTIVE + return STATUS_CLOSE diff --git a/icons/obj/casino.dmi b/icons/obj/casino.dmi index 37b7b420d89bafdf8ad8428454635380d3e37852..bf49dbd1b83311931054de4d05db0e497c17664c 100644 GIT binary patch delta 16038 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%0Dt57O&x?2xI4-H!H9h8?M~GHWR)~x5kMHu2egP z!BtjLCQIBNU_8LBSk6-xD5z7`81M4mp+SwGvFy2-R4>+yQ*g6kD;1yL-}bK0_O-5K zLAVg)32G!&C9Gm7wcu^y^H&J z(q{x;eoQW1zBxik=y1)bsIcx)mUX#W85rS3`zV>+meaq;l>YJpY4t$`$>`=$mqma2 zRjSB5h3-mqq8gQvWd_N<3VH$Gpc5Ji&Ctn|tI!AfBsXX52_5?A?GG612(qspUR}6P zA7!g$1KlXVao6@s))8RlTGAMnx-p_~4^5pLH#(fM2m?^1T0a4ladXIQaFii;qP%6q z&!0A*Y*qyHKcQ|f{`~x|$odg<_~1Z|(HzoCHwB1RqC<54lN4K^!%zrNH#i#W^lQu! z5LUiR`IQ?gCt5XZ4esqEpJ-b80wMYv$_3COAP_)#C98+MJWull4qhQqj?1H|QZuxa zDY_e;t>tn2vfiyUKl)b2?cW_kyX>7_U7H3KqC(AuyLP*mNI z{l<+OvT|}S#8@!bK?jGo(21}+IVR1fY*uBd^Dy2ej3GsIoCpnE-nMd|4y9&SPz;l` zSGw0m}d@^t^{p&}>bqXR^WTREVwnY%9HU zFs7*hb?;V#|EL7%T$4@N%}}SfC&jtuRBdx{zbP3=qXPBI1;SF{i{|8_pKz&6txngQM zPXGPFq!ZfUT<^63;H2ODehb6GJ`$>Y)2d!#I^^5>%o znt$5ymjhvZZL8WBXo3Z@DYe%CZE{VVtuGVPyT9^ZF-MPM937LF&9%{a3vmx|C~asKtS@S5cK9_m{+xh@ z>)(2mx}p1&z)%ID2~Q~|&>KUbHj6*i-^Omx;nzv@_cl-&Xf5Z_t`LysRL&bT|30h_ zkwMSjQx@KX;M*452S(bZ7hXTxmrvCSTGxRP;Hs_d(}DtdpbiOcLy}pnn3fMNbn`6( zkMFHIw_0b+1oR^t*JpZ^ZAs@t+j%U8ui^c8fOHmOK425NF79rGJ&@Fz9i$<;CbY4y zG18I~b2a}{U)f7BRE4gt7T{PO6%+FU&;^&fU?xQ}>CcJ8J_bkk-n4fnhN#a*DsL|R!|Bg6h-S=!?fsW8{f^QYBffU6bPqMh zgg%AvSX@V!fUq99{1{PWxo|MC)`wAR|`0T*gkyII>( zFkGnVh%m(GGkZQXhCcsNxpneI9llOkm1SUURF!*{M$l$Ehi$KgUp&`0=Z-@uB zKfli_tsT5*E2JyLNi8Z1>DkH1?JiNAzQw_1~w8XVRC@3=JxVn@8?G_m>pE2c7GK zW#vD_O%{b{U$cCWRV)Y@P2$ukPDqhT2G1acn>G|9TxU5MA;(b~6qlqI$X>8~a9H;b z--5Obt;clT_%`i`$kB}hBr9zTnKSn2YmP#CJ4uX1w%>olo(gkJ>G;YVc^p3j1rWx)ATaTZ7@Ps>wE zucVyHqs^`k_&~M*Sd!?S^K2zh;BlQk*^)X9U*$vLSFt;U8QQ}8E0O<^#A9Z$ssDYy z4ic0~X&hof(-P)hUJBfz>i3;u_uN1AO87Es|IxyF~?tN@P{AG%H3TXc=NMPQJ z-({Z+YMQSUy7awCz`xu2AW}-_=71P z)e)RYA9QrjSJC}<0n3{w1imb8Lp%ZiylW~>04@pX+zTUPv%mef>~X#jwM2mD+jchc zqvdt+FNQSAX9AqJ`$Rsl0a>c7=NJs{_|9H@x8o;u%Uk7?6_khLFub2Sodb`f{PRd{ zBygyEbMB>xZSHx9^Wk?m-}3_v8y!9S5`(FloOF0WMB2-25Z}RKgci{ws zG&ZYnLxCKBz4)3+`X54SCYg9HMs@2EkrLb80&sG IXCLwZ02uWFU;qFB delta 14905 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 !^ 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 diff --git a/icons/obj/tables.dmi b/icons/obj/tables.dmi index 05a486a08f18f039ef0b6513060c108eec1e50d0..8555d99c51d94c4ee6587d5c9547f1f5e6e32778 100644 GIT binary patch literal 31925 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 VXP$`c#X-Oy1Dy-!i_SWP{~xgjTr~gy literal 33247 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 diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/CommonComponents.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/CommonComponents.tsx new file mode 100644 index 0000000000..0e451a432d --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/CommonComponents.tsx @@ -0,0 +1,18 @@ +import { Icon, Tooltip } from 'tgui-core/components'; + +export const CastlingUsed = (props: { used: boolean }) => { + const { used } = props; + + return ( + + {used ? ( + + + + + ) : ( + + )} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/GameArea.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/GameArea.tsx new file mode 100644 index 0000000000..945adfb17f --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/GameArea.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import { numToLetter } from '../SpaceBattle/constants'; +import { checkDisabled, getGameIcons } from './functions'; +import { PromotionSelection } from './PromotionSelection'; +import type { Data } from './types'; + +export const GameArea = (props) => { + const { act, data } = useBackend(); + + const { + game_type, + game_state, + current_board, + selected_figure, + valid_moves, + possible_jumps, + } = data; + const [promotionFloating, setPromotionFloating] = + useState(null); + + function handleClick(isValidMove: boolean, x: number, y: number) { + if (selected_figure.length) { + const piece = + current_board[selected_figure[1] - 1][selected_figure[0] - 1]; + + const isPawnPromotion = + piece && + piece[1] === 'P' && + ((piece[0] === 'w' && y === 1) || (piece[0] === 'b' && y === 8)); + + if (isValidMove && isPawnPromotion && game_type === 'chess') { + setPromotionFloating( + , + ); + return; + } + } + + act('game_action', { + action: isValidMove ? 'move_figure' : 'select_figure', + data: { loc_x: x, loc_y: y }, + }); + } + + return ( +

{ + event.preventDefault(); + }} + > + + {game_state === 2 && promotionFloating} + + + + {Array.from({ length: 9 }, (_, row) => ( + + {Array.from({ length: 9 }, (_, col) => { + if (col === 0 && row === 0) { + return ( + + + + ); + } else if (col === 0 && row > 0) { + return ( + + {numToLetter[row - 1]} + + ); + } else if (row === 0 && col > 0) { + return ( + + {col} + + ); + } + + const boardX = row - 1; + const boardY = 8 - col; + const x = boardX + 1; + const y = boardY + 1; + const iconText = current_board.length + ? current_board[boardY][boardX] + : ''; + const isValidMove = valid_moves.some( + ([moveX, moveY]) => moveX === x && moveY === y, + ); + const canJump = possible_jumps?.some( + ([moveX, moveY]) => moveX === x && moveY === y, + ); + + return ( + + + + ); + })} + + ))} + + + + {game_state === 1 && promotionFloating} + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/PlayerMenu.tsx new file mode 100644 index 0000000000..c7535e9ef4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/PlayerMenu.tsx @@ -0,0 +1,126 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameTooltip } from './constants'; +import { gameStateToText, stateToColor } from './functions'; +import { PlayerPanel } from './PlayerPanel'; +import type { Data } from './types'; + +export const PlayerMenu = (props) => { + const { act, data } = useBackend(); + + const { + game_type, + game_state, + player_one, + player_two, + player_one_time, + player_two_time, + has_won, + game_flags = 0, + winner, + } = data; + + return ( +
+ {(game_state > 0 && ( + + { + act('clear_game'); + }} + > + Clear Game + + + )) || + (game_state === 0 && !!player_one && !!player_two && ( + <> + + { + act('start_game'); + }} + > + Start Game + + + + { + act('swap_players'); + }} + > + Swap Players + + + + ))} + {!!player_one && !!player_two && game_state === 3 && ( + <> + + { + act('play_again'); + }} + > + Play Again + + + + { + act('play_again_swapped'); + }} + > + Play Again (Swapped Sides) + + + + )} + + + + + + + + } + > + + + + + + Status: + + + + {gameStateToText(game_state, game_flags, winner)} + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/PlayerPanel.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/PlayerPanel.tsx new file mode 100644 index 0000000000..b35641ae69 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/PlayerPanel.tsx @@ -0,0 +1,50 @@ +import { useBackend } from 'tgui/backend'; +import { Button, ColorBox, LabeledList, Stack } from 'tgui-core/components'; +import { formatTime } from 'tgui-core/format'; +import { numToText } from '../SpaceBattle/constants'; +import { CastlingUsed } from './CommonComponents'; +import type { Data } from './types'; + +export const PlayerPanel = (props: { + gameState: number; + player: string | null; + time: number; + castlingUsed: boolean; + num: number; +}) => { + const { data, act } = useBackend(); + const { game_type } = data; + const { gameState, player, time, castlingUsed, num } = props; + + const player_string = numToText[num - 1]; + + return ( + + + + act(`be_player_${player_string}`)} + > + {player || `Be player ${num}`} + + + + + + {formatTime(time)} + {game_type === 'chess' && ( + + + + )} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/PromotionSelection.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/PromotionSelection.tsx new file mode 100644 index 0000000000..84e21674dc --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/PromotionSelection.tsx @@ -0,0 +1,48 @@ +import { useBackend } from 'tgui/backend'; +import { Button, Stack } from 'tgui-core/components'; +import { possiblePromotions } from './constants'; +import { getGameIcons } from './functions'; +import type { Data } from './types'; + +export const PromotionSelection = (props: { + x: number; + y: number; + iconText: string; + onPromotionFloating: React.Dispatch< + React.SetStateAction + >; +}) => { + const { act, data } = useBackend(); + const { x, y, iconText, onPromotionFloating } = props; + return ( + + {possiblePromotions.map((option) => ( + + + + ))} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/constants.ts b/tgui/packages/tgui/interfaces/ChessCheckers/constants.ts new file mode 100644 index 0000000000..21098f29a1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/constants.ts @@ -0,0 +1,32 @@ +export const textToChess = { + wK: '♔', + wQ: '♕', + wR: '♖', + wB: '♗', + wN: '♘', + wP: '♙', + + bK: '♚', + bQ: '♛', + bR: '♜', + bB: '♝', + bN: '♞', + bP: '♟', +} as const; + +export const textToCheckers = { + wM: '⛀', + wK: '⛁', + + bM: '⛂', + bK: '⛃', +} as const; + +export const possiblePromotions = ['Q', 'R', 'B', 'N'] as const; + +export const gameTooltip = { + chess: + "To play, the player's pick their side, white or black and then play in turns. Common chess rules apply.", + checkers: + "To play, the player's pick their side, white or black and then play in turns. Common checkers rules apply.", +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/functions.ts b/tgui/packages/tgui/interfaces/ChessCheckers/functions.ts new file mode 100644 index 0000000000..0bd516e0b3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/functions.ts @@ -0,0 +1,74 @@ +import { textToCheckers, textToChess } from './constants'; + +export function getGameIcons( + type: string, + currentPosition: string | null, +): string { + if (!currentPosition) { + return ''; + } + switch (type) { + case 'chess': + return textToChess[currentPosition]; + case 'checkers': + return textToCheckers[currentPosition]; + default: + return ''; + } +} + +export function stateToColor( + gameState: number, + gameFlags: number, + isWinner: boolean, +): string | undefined { + if (gameState === 0) { + return undefined; + } + if (gameState > 0 && gameState < 3) { + if ( + (gameState === 1 && gameFlags & 1) || + (gameState === 2 && gameFlags & 2) + ) { + return 'yellow'; + } + return undefined; + } + if (gameState === 3) { + return isWinner ? 'green' : 'red'; + } + return 'yellow'; +} + +export function gameStateToText( + gameState: number, + gameFlags: number, + winner: string | null, +): string { + if (gameState === 0) { + return 'Setup'; + } + if (gameState === 1) { + if (gameFlags & 1) { + return 'Player 1 (Check)'; + } + return 'Player 1'; + } + if (gameState === 2) { + if (gameFlags & 2) { + return 'Player 2 (Check)'; + } + return 'Player 2'; + } + if (gameState >= 3) { + return `${winner} ${gameState === 3 ? 'Won' : 'Draw'}`; + } + return ''; +} + +export function checkDisabled(gameState: number): boolean { + if (gameState < 1 || gameState > 2) { + return true; + } + return false; +} diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/index.tsx b/tgui/packages/tgui/interfaces/ChessCheckers/index.tsx new file mode 100644 index 0000000000..4ef0741857 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/index.tsx @@ -0,0 +1,21 @@ +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; +import { GameArea } from './GameArea'; +import { PlayerMenu } from './PlayerMenu'; + +export const ChessCheckers = (props) => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ChessCheckers/types.ts b/tgui/packages/tgui/interfaces/ChessCheckers/types.ts new file mode 100644 index 0000000000..f7e667f82c --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChessCheckers/types.ts @@ -0,0 +1,17 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + game_type: string; + player_one: string; + player_two: string; + player_one_time: number; + player_two_time: number; + current_board: (string | null)[][]; + selected_figure: number[]; + valid_moves: number[][]; + game_state: number; + winner: string; + has_won: BooleanLike; + game_flags?: number; + possible_jumps?: number[][]; +}; diff --git a/tgui/packages/tgui/interfaces/FourInARow/GameArea.tsx b/tgui/packages/tgui/interfaces/FourInARow/GameArea.tsx new file mode 100644 index 0000000000..95bd0c5ad6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/GameArea.tsx @@ -0,0 +1,122 @@ +import type { CSSProperties } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Button, Icon, Section, Stack } from 'tgui-core/components'; +import { classes } from 'tgui-core/react'; +import type { Data } from './types'; + +export const GameArea = (props) => { + const { act, data } = useBackend(); + + const { + grid_x_size, + grid_y_size, + placed_chips_pone, + placed_chips_ptwo, + player_one_color, + player_two_color, + game_state, + winning_tiles, + } = data; + + function playerColor(key: string): string { + if (placed_chips_pone[key]) { + return player_one_color; + } + if (placed_chips_ptwo[key]) { + return player_two_color; + } + return 'black'; + } + + const isTicTacToe = grid_x_size === 3; + function playerIcon(key: string): React.JSX.Element | null { + if (placed_chips_pone[key]) { + return ; + } + if (placed_chips_ptwo[key]) { + return ; + } + return null; + } + + function isWinningTile(key: string): boolean { + return game_state === 3 && winning_tiles?.includes(key); + } + + return ( +
{ + event.preventDefault(); + }} + > + + + + + {Array.from({ length: grid_y_size }, (_, row) => ( + + {Array.from({ length: grid_x_size }, (_, col) => { + const x = col + 1; + const y = row + 1; + const key = `${x},${y}`; + const tileClasses = isTicTacToe + ? ['TicTacToe__Tile'] + : ['FourInARow__Tile']; + + let dynamicStyle: CSSProperties = {}; + + if (isWinningTile(key)) { + tileClasses.push('winning-tile'); + dynamicStyle = { + background: `radial-gradient(circle at 50% 50%, yellow 0%, ${playerColor(key)}, red 100%)`, + }; + } else if ( + placed_chips_pone[key] || + placed_chips_ptwo[key] + ) { + tileClasses.push('placed'); + dynamicStyle = { + background: `radial-gradient(circle at 30% 30%, white 0%, ${playerColor(key)} 70%)`, + }; + } + + return ( + + + + ); + })} + + ))} + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/FourInARow/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/FourInARow/PlayerMenu.tsx new file mode 100644 index 0000000000..31713b68f1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/PlayerMenu.tsx @@ -0,0 +1,177 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, NumberInput, Section, Stack } from 'tgui-core/components'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameTooltip } from './constants'; +import { gameStateToText, stateToColor } from './functions'; +import { PlayerPanel } from './PlayerPanel'; +import type { Data } from './types'; + +export const PlayerMenu = (props) => { + const { act, data } = useBackend(); + + const { + colors, + player_one, + player_two, + game_state, + grid_x_size, + win_count, + player_one_color, + player_two_color, + winner, + has_won, + } = data; + + const isFourRow = grid_x_size > 3; + const isTicTacToe = !isFourRow; + + return ( +
+ {(game_state > 0 && ( + + { + act('clear_game'); + }} + > + Clear Game + + + )) || + (game_state === 0 && player_one && player_two && ( + <> + + { + act('start_game'); + }} + > + Start Game + + + + { + act('swap_players'); + }} + > + Swap Players + + + + ))} + {!!player_one && !!player_two && game_state === 3 && ( + <> + + { + act('play_again'); + }} + > + Play Again + + + + { + act('play_again_swapped'); + }} + > + Play Again (Swapped Sides) + + + + )} + + + + + + + + } + > + + c !== player_two_color)} + num={1} + /> + + + + + + Size: + + + {game_state === 0 && isFourRow ? ( + act('change_size', { size: value })} + /> + ) : ( + grid_x_size + )} + + + + Win Con.: + + + {game_state === 0 && isFourRow ? ( + act('change_win', { count: value })} + /> + ) : ( + win_count + )} + + + + + Status: + + + + {gameStateToText(game_state, winner)} + + + + + c !== player_one_color)} + num={2} + /> + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/FourInARow/PlayerPanel.tsx b/tgui/packages/tgui/interfaces/FourInARow/PlayerPanel.tsx new file mode 100644 index 0000000000..f6b976e360 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/PlayerPanel.tsx @@ -0,0 +1,67 @@ +import { useBackend } from 'tgui/backend'; +import { + Button, + ColorBox, + Dropdown, + Icon, + LabeledList, + Stack, +} from 'tgui-core/components'; +import { numToText } from '../SpaceBattle/constants'; + +export const PlayerPanel = (props: { + ticTacToe: boolean; + gameState: number; + player: string | null; + color: string; + usableColors: string[]; + num: number; +}) => { + const { act } = useBackend(); + const { ticTacToe, gameState, player, color, usableColors, num } = props; + + const player_string = numToText[num - 1]; + + return ( + + + + act(`be_player_${player_string}`)} + > + {player || `Be player ${num}`} + + + + + + + act(`set_color_${player_string}`, { color: value }) + } + options={usableColors} + selected={color} + /> + + + {ticTacToe ? ( + + ) : ( + + )} + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/FourInARow/constants.ts b/tgui/packages/tgui/interfaces/FourInARow/constants.ts new file mode 100644 index 0000000000..9d8c369a71 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/constants.ts @@ -0,0 +1,5 @@ +export const gameTooltip = [ + 'A two player game where players take turns with the goal to place the previously set up amount of chips into a connected row. ' + + 'Left click onto the playfield to palce your chip.', + 'A game where players take turns with the goal to place their marks in a line across the board.', +]; diff --git a/tgui/packages/tgui/interfaces/FourInARow/functions.ts b/tgui/packages/tgui/interfaces/FourInARow/functions.ts new file mode 100644 index 0000000000..9200b46892 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/functions.ts @@ -0,0 +1,39 @@ +export function stateToColor( + gameState: number, + playerOneColor: string, + playeTwoColor: string, + isWinner: boolean, +): string | undefined { + if (gameState < 1) { + return undefined; + } + if (gameState === 1) { + return playerOneColor; + } + if (gameState === 2) { + return playeTwoColor; + } + if (gameState === 4) { + return 'yellow'; + } + return isWinner ? 'green' : 'red'; +} + +export function gameStateToText( + gameState: number, + winner: string | null, +): string { + if (gameState < 1) { + return 'Setup'; + } + if (gameState === 1) { + return 'Player 1'; + } + if (gameState === 2) { + return 'Player 2'; + } + if (gameState === 4) { + return 'Draw'; + } + return `${winner} Won`; +} diff --git a/tgui/packages/tgui/interfaces/FourInARow/index.tsx b/tgui/packages/tgui/interfaces/FourInARow/index.tsx new file mode 100644 index 0000000000..164dc9b39b --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/index.tsx @@ -0,0 +1,27 @@ +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; +import { GameArea } from './GameArea'; +import { PlayerMenu } from './PlayerMenu'; +import type { Data } from './types'; + +export const FourInARow = (props) => { + const { data } = useBackend(); + + const { grid_y_size } = data; + const added_height = grid_y_size * 63; + return ( + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/FourInARow/types.ts b/tgui/packages/tgui/interfaces/FourInARow/types.ts new file mode 100644 index 0000000000..bfe1cef147 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FourInARow/types.ts @@ -0,0 +1,18 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + colors: string[]; + player_one: string | null; + player_two: string | null; + placed_chips_pone: Record; + placed_chips_ptwo: Record; + game_state: number; + grid_x_size: number; + grid_y_size: number; + player_one_color: string; + player_two_color: string; + win_count: number; + winner: string | null; + has_won: BooleanLike; + winning_tiles: string[]; +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/GameArea.tsx b/tgui/packages/tgui/interfaces/NineMen/GameArea.tsx new file mode 100644 index 0000000000..02c55866a1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/GameArea.tsx @@ -0,0 +1,81 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section } from 'tgui-core/components'; +import { nodePositions, textToNine } from './constants'; +import { checkDisabled } from './functions'; +import type { Data } from './types'; + +export const GameArea = () => { + const { act, data } = useBackend(); + + const { + game_state, + current_board, + selected_node, + valid_moves, + valid_removes, + phase, + } = data; + + function handleClick(isValidMove: boolean, node: number) { + act('game_action', { + action: + (!selected_node || !isValidMove) && phase === 1 + ? 'select_figure' + : 'move_figure', + data: { node_number: node }, + }); + } + + return ( +
+ + + + + + + + + + + {Array.from({ length: 24 }, (_, i) => { + const node = i + 1; + const piece = current_board[i]; + const isSelected = selected_node === node; + const isValidMove = valid_moves?.includes(node); + const isValidRemove = valid_removes?.includes(node); + const pos = nodePositions[node]; + + return ( + + ); + })} + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/NineMen/PlayerMenu.tsx new file mode 100644 index 0000000000..93738fbdeb --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/PlayerMenu.tsx @@ -0,0 +1,125 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameTooltip, phastToText } from './constants'; +import { gameStateToText, stateToColor } from './functions'; +import { PlayerPanel } from './PlayerPanel'; +import type { Data } from './types'; + +export const PlayerMenu = (props) => { + const { act, data } = useBackend(); + + const { + game_state, + phase, + player_one, + player_two, + player_one_time, + player_two_time, + has_won, + winner, + pone_pieces, + ptwo_pieces, + } = data; + + return ( +
+ {(game_state > 0 && ( + + { + act('clear_game'); + }} + > + Clear Game + + + )) || + (game_state === 0 && !!player_one && !!player_two && ( + <> + + { + act('start_game'); + }} + > + Start Game + + + + { + act('swap_players'); + }} + > + Swap Players + + + + ))} + {!!player_one && !!player_two && game_state === 3 && ( + <> + + { + act('play_again'); + }} + > + Play Again + + + + { + act('play_again_swapped'); + }} + > + Play Again (Swapped Sides) + + + + )} + + + + + + + + } + > + + + + + + Status: + + + + {gameStateToText(game_state, winner)} + + + {phastToText[phase]} + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/PlayerPanel.tsx b/tgui/packages/tgui/interfaces/NineMen/PlayerPanel.tsx new file mode 100644 index 0000000000..60f97fafd2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/PlayerPanel.tsx @@ -0,0 +1,44 @@ +import { useBackend } from 'tgui/backend'; +import { Button, ColorBox, LabeledList, Stack } from 'tgui-core/components'; +import { formatTime } from 'tgui-core/format'; +import { numToText } from '../SpaceBattle/constants'; +import type { Data } from './types'; + +export const PlayerPanel = (props: { + gameState: number; + player: string | null; + pieces: number; + time: number; + num: number; +}) => { + const { act } = useBackend(); + const { gameState, player, pieces, time, num } = props; + + const player_string = numToText[num - 1]; + + return ( + + + + act(`be_player_${player_string}`)} + > + {player || `Be player ${num}`} + + + + + + {pieces} + {formatTime(time)} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/constants.ts b/tgui/packages/tgui/interfaces/NineMen/constants.ts new file mode 100644 index 0000000000..260525170f --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/constants.ts @@ -0,0 +1,42 @@ +export const textToNine = { + w: '⛀', + b: '⛂', +} as const; + +export const gameTooltip = + 'To play the game, players place their pieces in turns. When one player manages to place three pieces in a row, they remove an opponent piece. After a player places 9 pieces, they must move their pieces to open, adjacent positions. If a player only has 3 pieces, they can jump any piece to any open space. A player loses as soon as less than three pieces are left.'; + +export const phastToText = ['Place', 'Move', 'Remove', ''] as const; + +export const nodePositions = { + 1: { x: 1, y: 1 }, + 2: { x: 50, y: 1 }, + 3: { x: 99, y: 1 }, + + 4: { x: 17, y: 17 }, + 5: { x: 50, y: 17 }, + 6: { x: 83, y: 17 }, + + 7: { x: 33, y: 33 }, + 8: { x: 50, y: 33 }, + 9: { x: 67, y: 33 }, + + 10: { x: 1, y: 50 }, + 11: { x: 17, y: 50 }, + 12: { x: 33, y: 50 }, + 13: { x: 67, y: 50 }, + 14: { x: 83, y: 50 }, + 15: { x: 99, y: 50 }, + + 16: { x: 33, y: 67 }, + 17: { x: 50, y: 67 }, + 18: { x: 67, y: 67 }, + + 19: { x: 17, y: 83 }, + 20: { x: 50, y: 83 }, + 21: { x: 83, y: 83 }, + + 22: { x: 1, y: 99 }, + 23: { x: 50, y: 99 }, + 24: { x: 99, y: 99 }, +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/functions.ts b/tgui/packages/tgui/interfaces/NineMen/functions.ts new file mode 100644 index 0000000000..dd0811cea0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/functions.ts @@ -0,0 +1,38 @@ +export function stateToColor( + gameState: number, + isWinner: boolean, +): string | undefined { + if (gameState === 0) { + return undefined; + } + if (gameState === 3) { + return isWinner ? 'green' : 'red'; + } + return undefined; +} + +export function gameStateToText( + gameState: number, + winner: string | null, +): string { + if (gameState === 0) { + return 'Setup'; + } + if (gameState === 1) { + return 'Player 1'; + } + if (gameState === 2) { + return 'Player 2'; + } + if (gameState >= 3) { + return `${winner} ${gameState === 3 ? 'Won' : 'Draw'}`; + } + return ''; +} + +export function checkDisabled(gameState: number): boolean { + if (gameState < 1 || gameState > 2) { + return true; + } + return false; +} diff --git a/tgui/packages/tgui/interfaces/NineMen/index.tsx b/tgui/packages/tgui/interfaces/NineMen/index.tsx new file mode 100644 index 0000000000..1229fd81ba --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/index.tsx @@ -0,0 +1,21 @@ +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; +import { GameArea } from './GameArea'; +import { PlayerMenu } from './PlayerMenu'; + +export const NineMen = (props) => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/NineMen/types.ts b/tgui/packages/tgui/interfaces/NineMen/types.ts new file mode 100644 index 0000000000..c5b0cfc6fb --- /dev/null +++ b/tgui/packages/tgui/interfaces/NineMen/types.ts @@ -0,0 +1,18 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + player_one: string; + player_two: string; + player_one_time: number; + player_two_time: number; + current_board: (string | null)[]; + selected_node: number | null; + valid_moves: number[]; + valid_removes: number[]; + game_state: number; + winner: string; + has_won: BooleanLike; + phase: number; + pone_pieces: number; + ptwo_pieces: number; +}; diff --git a/tgui/packages/tgui/interfaces/Pda/pda_screens/pda_game_launcher.tsx b/tgui/packages/tgui/interfaces/Pda/pda_screens/pda_game_launcher.tsx new file mode 100644 index 0000000000..e4da97cfd5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Pda/pda_screens/pda_game_launcher.tsx @@ -0,0 +1,49 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, LabeledList, Section, Stack } from 'tgui-core/components'; + +type Data = { + available_games: Record; +}; + +export const pda_game_launcher = (props) => { + const { act, data } = useBackend(); + + const { available_games } = data; + + return ( + +
+ + + Play your favourite games everywhere, any time! Don't get caught! + + + {Object.keys(available_games).map((game) => { + const gameAction = available_games[game]; + return ( + + + + + + {!!gameAction && ( + + act(game, { close: true })} + icon="circle-xmark" + iconColor="red" + color="transparent" + tooltip="forcefully close the game" + /> + + )} + + + ); + })} + + +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/RpgDice.tsx/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/RpgDice.tsx/PlayerMenu.tsx new file mode 100644 index 0000000000..85ce6bba2f --- /dev/null +++ b/tgui/packages/tgui/interfaces/RpgDice.tsx/PlayerMenu.tsx @@ -0,0 +1,248 @@ +import { type RefObject, useRef, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { + Button, + LabeledList, + NumberInput, + Section, + Stack, +} from 'tgui-core/components'; +import { KEY } from 'tgui-core/keys'; +import { classes } from 'tgui-core/react'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameTooltip, MAX_HISTORY, presetDice } from './constants'; +import type { Data } from './types'; + +export const PlayerMenu = (props) => { + const { act } = useBackend(); + const [diceSize, setDiceSize] = useState(6); + const [diceCount, setDiceCount] = useState(1); + const [modifier, setModifier] = useState(0); + const [applyToAll, setApplyToAll] = useState(false); + const [customInput, setCustomInput] = useState(''); + + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + + const inputRef: RefObject = useRef(null); + + function updateHistory(newEntry: string) { + if (newEntry === history[historyIndex]) return; + + const newHistory = [...history, newEntry]; + + if (newHistory.length > MAX_HISTORY) { + newHistory.shift(); + } + + setHistory(newHistory); + setHistoryIndex(newHistory.length); + } + + function handleKeyDown(event: React.KeyboardEvent): void { + if (event.getModifierState('AltGraph')) return; + + switch (event.key) { + case KEY.PageUp: + event.preventDefault(); + inputRef.current?.blur(); + handleUndo(); + requestAnimationFrame(() => inputRef.current?.focus()); + break; + case KEY.PageDown: + event.preventDefault(); + inputRef.current?.blur(); + handleRedo(); + requestAnimationFrame(() => inputRef.current?.focus()); + break; + } + } + + function handleUndo() { + if (historyIndex > 0) { + const prevState = history[historyIndex - 1]; + setCustomInput(prevState); + setHistoryIndex(historyIndex - 1); + } + requestAnimationFrame(() => inputRef.current?.focus()); + } + + function handleRedo() { + if (historyIndex < history.length) { + const nextState = history[historyIndex + 1]; + setCustomInput(nextState); + setHistoryIndex(historyIndex + 1); + } else if (historyIndex === history.length) { + setCustomInput(''); + } + requestAnimationFrame(() => inputRef.current?.focus()); + } + + const firstHalf = presetDice.slice(0, 4); + const secondHalf = presetDice.slice(4); + return ( +
+ + + + + + + + } + > + + + + {firstHalf.map((dice, index) => ( + + + + ))} + + + + + {secondHalf.map((dice, index) => ( + + + + ))} + + + + + + + + + + + + + + + + + + + setApplyToAll(!applyToAll)} + > + Apply to all + + + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/RpgDice.tsx/RollArea.tsx b/tgui/packages/tgui/interfaces/RpgDice.tsx/RollArea.tsx new file mode 100644 index 0000000000..1dc3d37387 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RpgDice.tsx/RollArea.tsx @@ -0,0 +1,118 @@ +import { Fragment, type RefObject, useEffect, useRef } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import { stateToColor } from './constants'; +import type { Data, Result } from './types'; + +export const RollArea = (props) => { + const { act, data } = useBackend(); + + const { last_rolls } = data; + + const messagesEndRef: RefObject = useRef(null); + + useEffect(() => { + const scroll = messagesEndRef.current; + if (!scroll) return; + + const isAtBottom = + Math.abs(scroll.scrollHeight - scroll.scrollTop - scroll.offsetHeight) < + 48; + + if (isAtBottom) { + scroll.scrollTop = scroll.scrollHeight; + } + }, [last_rolls]); + + return ( +
act('clear_history')} icon="trash" /> + } + > + + {last_rolls.map((roll, index) => ( + + + {roll.player}: + + + {roll.count} + + {!!roll.size && ( + + d{roll.size} + + )} + {!!roll.mod && ( + + +{roll.mod} + + )} + + {`: `} + + {!!roll.results && ( + + )} + {(!roll.results || roll.results.length > 1 || !!roll.mod) && ( + + {roll.sum} + + )} + + ))} + +
+ ); +}; + +export const DetailedResults = (props: { + results: Result[]; + mod: number; + applyToAll: boolean; +}) => { + const { results, mod, applyToAll } = props; + + const moreThanOne = results.length > 1; + + return ( + <> + {!!mod && !applyToAll && moreThanOne && {`(`}} + {results.map((result, index) => ( + + {!!mod && applyToAll && moreThanOne && {`(`}} + + {result.result} + + + {!!mod && applyToAll && ` + ${mod}`} + {!!mod && applyToAll && !!moreThanOne && `)`} + + {(moreThanOne || !!mod) && + (index < results.length - 1 ? ( + {` + `} + ) : ( + + {!!mod && !applyToAll && !!moreThanOne && `)`} + {!!mod && !applyToAll && ` + ${mod}`} + {` = `} + + ))} + + ))} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RpgDice.tsx/constants.ts b/tgui/packages/tgui/interfaces/RpgDice.tsx/constants.ts new file mode 100644 index 0000000000..cdb3d95c63 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RpgDice.tsx/constants.ts @@ -0,0 +1,9 @@ +export const presetDice = [4, 6, 8, 10, 12, 20, 50, 100] as const; + +export const gameTooltip = + 'A simple dice roller. Use predefined dice sizes and counts to get a detailed breakdown of the rolls. Or use a fully custom query like 1d10 + 5 + 2d5 - 6. ' + + "For those won't be any per dice breakdown."; + +export const MAX_HISTORY = 10; + +export const stateToColor = [undefined, 'red', 'green'] as const; diff --git a/tgui/packages/tgui/interfaces/RpgDice.tsx/index.tsx b/tgui/packages/tgui/interfaces/RpgDice.tsx/index.tsx new file mode 100644 index 0000000000..2cf6dafd80 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RpgDice.tsx/index.tsx @@ -0,0 +1,21 @@ +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; +import { PlayerMenu } from './PlayerMenu'; +import { RollArea } from './RollArea'; + +export const RpgDice = (props) => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/RpgDice.tsx/types.ts b/tgui/packages/tgui/interfaces/RpgDice.tsx/types.ts new file mode 100644 index 0000000000..52f0597333 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RpgDice.tsx/types.ts @@ -0,0 +1,15 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + last_rolls: { + player: string; + count: number; + size?: number; + results?: Result[]; + mod?: number; + apply_to_all?: BooleanLike; + sum: number; + }[]; +}; + +export type Result = { result: number; state: number }; diff --git a/tgui/packages/tgui/interfaces/SpaceBattle/GameArea.tsx b/tgui/packages/tgui/interfaces/SpaceBattle/GameArea.tsx new file mode 100644 index 0000000000..d35672c2af --- /dev/null +++ b/tgui/packages/tgui/interfaces/SpaceBattle/GameArea.tsx @@ -0,0 +1,277 @@ +import { useEffect, useRef, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import { numToLetter } from './constants'; +import { generateShipCoordinates } from './functions'; +import type { Data, Ship } from './types'; + +export const GameArea = (props: { + playerOnePlaceShip: string; + playerTwoPlaceShip: string; + playerOneOrientation: 'vertical' | 'horizontal'; + playerTwoOrientation: 'vertical' | 'horizontal'; +}) => { + const { data } = useBackend(); + + const { + current_player, + player_one, + player_two, + shots_fired_pone, + shots_fired_ptwo, + destroyed_ships_pone, + destroyed_ships_ptwo, + ship_count_pone, + ship_count_ptwo, + } = data; + + const { + playerOnePlaceShip, + playerTwoPlaceShip, + playerOneOrientation, + playerTwoOrientation, + } = props; + + return ( +
{ + event.preventDefault(); + }} + > + + + + + + + + + +
+ ); +}; + +const Playfield = (props: { + player: number; + isOponent: boolean; + isSelf: boolean; + shotsFired: Record; + destroyedShips: Ship[]; + shipBeingPlaced: string; + orientation: 'vertical' | 'horizontal'; + shipCount: Record; +}) => { + const { data, act } = useBackend(); + const { game_state, ship_sizes, visible_ships } = data; + const { + player, + isOponent, + isSelf, + shotsFired, + destroyedShips, + shipBeingPlaced, + orientation, + shipCount, + } = props; + + const [highlightedCells, setHighlightedCells] = useState< + { x: number; y: number }[] + >([]); + const [invalidCells, setInvalidCells] = useState(false); + const lastLoc = useRef<[number, number] | null>(null); + + const handleButtonHover = (x: number, y: number) => { + setHighlightedCells([]); + if (!shipCount[shipBeingPlaced]) { + return; + } + const size = ship_sizes[shipBeingPlaced]; + const newHighlightedCells: { x: number; y: number }[] = []; + let isInvalid = false; + lastLoc.current = [x, y]; + + for (let i = 0; i < size; i++) { + const newX = orientation === 'horizontal' ? x + i : x; + const newY = orientation === 'vertical' ? y + i : y; + + if (newX < 1 || newY < 1 || newX >= 11 || newY >= 11) { + isInvalid = true; + break; + } + for (const ship of visible_ships) { + if ( + ship.coords?.some( + (coord) => coord[0] === newX && coord[1] === newY, + ) && + ship.player === player + ) { + isInvalid = true; + break; + } + } + + newHighlightedCells.push({ x: newX, y: newY }); + } + + setHighlightedCells(newHighlightedCells); + setInvalidCells(isInvalid); + }; + + useEffect(() => { + if (lastLoc.current) { + handleButtonHover(lastLoc.current[0], lastLoc.current[1]); + } + }, [orientation, shipCount, shipBeingPlaced]); + + function handlePlacement(x: number, y: number) { + const shipCoords = generateShipCoordinates( + x, + y, + ship_sizes[shipBeingPlaced], + orientation, + ); + + if (game_state === 1 && isSelf) { + const shipData = { + player: player, + name: shipBeingPlaced, + coords: shipCoords, + }; + + act('place_ship', { + ship: shipData, + }); + } + } + + function getCellStyle(x: number, y: number) { + const key = `${x},${y}`; + let cellStyle: React.CSSProperties = {}; + + if (visible_ships) { + for (const ship of visible_ships) { + if ( + ship.coords?.some((coord) => coord[0] === x && coord[1] === y) && + ship.player === player + ) { + cellStyle.backgroundColor = 'gray'; + } + } + } + + if (game_state >= 2) { + const shot = shotsFired[key]; + if (shot === 1) { + cellStyle.backgroundColor = 'red'; + } else if (shot === 0) { + cellStyle.backgroundColor = 'white'; + } + } + + for (const ship of destroyedShips) { + if (ship.coords?.some((coord) => coord[0] === x && coord[1] === y)) { + cellStyle = { + border: '2px solid gold', + backgroundColor: 'maroon', + }; + } + } + + return cellStyle; + } + + return ( + + + {Array.from({ length: 11 }, (_, row) => ( + + {Array.from({ length: 11 }, (_, col) => { + const x = col; + const y = row; + const key = `${x},${y}`; + + const isHighlighted = + highlightedCells.some((cell) => cell.x === x && cell.y === y) && + game_state === 1 && + isSelf; + + return ( + + {col === 0 && row === 0 ? ( + + ) : col === 0 && row > 0 ? ( + {row - 1} + ) : row === 0 && col > 0 ? ( + {numToLetter[col - 1]} + ) : ( +
handleButtonHover(x, y)}> +
+ )} +
+ ); + })} +
+ ))} +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/SpaceBattle/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/SpaceBattle/PlayerMenu.tsx new file mode 100644 index 0000000000..82d02cf1d5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/SpaceBattle/PlayerMenu.tsx @@ -0,0 +1,166 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Button, Section, Stack } from 'tgui-core/components'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameStateToText, gameTooltip } from './constants'; +import { stateToColor } from './functions'; +import { PlayerPanel } from './PlayerPanel'; +import type { Data } from './types'; + +export const PlayerMenu = (props: { + playerOnePlaceShip: string; + onPlayerOnePlaceShip: React.Dispatch>; + playerTwoPlaceShip: string; + onPlayerTwoPlaceShip: React.Dispatch>; + playerOneOrientation: 'horizontal' | 'vertical'; + onPlayerOneOrientation: React.Dispatch< + React.SetStateAction<'horizontal' | 'vertical'> + >; + playerTwoOrientation: 'horizontal' | 'vertical'; + onPlayerTwoOrientation: React.Dispatch< + React.SetStateAction<'horizontal' | 'vertical'> + >; +}) => { + const { act, data } = useBackend(); + + const { + ship_sizes, + player_one, + player_two, + all_placed, + ship_count_pone, + ship_count_ptwo, + game_state, + winner, + has_won, + } = data; + + const { + playerOnePlaceShip, + onPlayerOnePlaceShip, + playerTwoPlaceShip, + onPlayerTwoPlaceShip, + playerOneOrientation, + onPlayerOneOrientation, + playerTwoOrientation, + onPlayerTwoOrientation, + } = props; + + return ( +
+ {(game_state > 0 && ( + + { + act('clear_game'); + }} + > + Clear Game + + + )) || + (game_state === 0 && !!player_one && !!player_two && ( + <> + + { + act('prepare_game'); + }} + > + Prepare Game + + + + { + act('swap_players'); + }} + > + Swap Players + + + + ))} + {game_state === 1 && !!player_one && !!player_two && !!all_placed && ( + + { + act('start_game'); + }} + > + Start Game + + + )} + {!!player_one && !!player_two && game_state === 4 && ( + <> + + { + act('play_again'); + }} + > + Play Again + + + + { + act('play_again_swapped'); + }} + > + Play Again (Swapped Sides) + + + + )} + + + + + + + + } + > + + + + + + Status: + + + + {gameStateToText[game_state] ?? `${winner} Won`} + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/SpaceBattle/PlayerPanel.tsx b/tgui/packages/tgui/interfaces/SpaceBattle/PlayerPanel.tsx new file mode 100644 index 0000000000..a1b0c9d7e5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/SpaceBattle/PlayerPanel.tsx @@ -0,0 +1,96 @@ +import { useEffect } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Button, LabeledList, Stack } from 'tgui-core/components'; +import { numToText } from './constants'; +import { getNextAvailableShip } from './functions'; +import type { Data } from './types'; + +export const PlayerPanel = (props: { + gameState: number; + player: string | null; + num: number; + availableShips: Record; + shipCount: Record; + playerPlaceShip: string; + onPlayerPlaceShip: React.Dispatch>; + playerRotation: 'horizontal' | 'vertical'; + onPlayerRotation: React.Dispatch< + React.SetStateAction<'horizontal' | 'vertical'> + >; +}) => { + const { data, act } = useBackend(); + const { current_player } = data; + const { + gameState, + player, + num, + shipCount, + availableShips, + playerPlaceShip, + onPlayerPlaceShip, + playerRotation, + onPlayerRotation, + } = props; + + const player_string = numToText[num - 1]; + + useEffect(() => { + if (gameState !== 1) return; + console.log(shipCount[playerPlaceShip]); + if (!shipCount[playerPlaceShip]) { + const nextShip = getNextAvailableShip(playerPlaceShip, shipCount); + if (nextShip) { + onPlayerPlaceShip(nextShip); + } + } + }, [gameState, playerPlaceShip, shipCount, onPlayerPlaceShip]); + + return ( + + + + + + act(`be_player_${player_string}`)} + > + {player || `Be player ${num}`} + + + + + + ); + })} + + ))} + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/VoreSweeper/PlayerMenu.tsx b/tgui/packages/tgui/interfaces/VoreSweeper/PlayerMenu.tsx new file mode 100644 index 0000000000..e3e1a56240 --- /dev/null +++ b/tgui/packages/tgui/interfaces/VoreSweeper/PlayerMenu.tsx @@ -0,0 +1,191 @@ +import { useBackend } from 'tgui/backend'; +import { + Box, + Button, + Icon, + LabeledList, + NumberInput, + Section, + Stack, +} from 'tgui-core/components'; +import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip'; +import { gameStateToColor, gameStateToIcon, gameTooltip } from './constants'; +import { getRemainingMines, getScoreColor } from './functions'; +import type { Data } from './types'; + +export const PlayerMenu = (props) => { + const { act, data } = useBackend(); + + const { + grid_size, + mine_count, + max_mines, + dealer, + placed_mines, + placed_flags, + game_state, + is_dealer, + } = data; + + const remainingMines = getRemainingMines( + game_state, + mine_count, + !!is_dealer, + placed_mines, + placed_flags, + ); + + return ( +
+ + {(game_state > 0 && ( + { + act('restart_game'); + }} + > + Restart Game + + )) || + (game_state === 0 && dealer && !remainingMines && ( + { + act('setup_action', { action: 'start_game' }); + }} + > + Start Game + + ))} + + + + + + + + + } + > + + + + + {dealer ? ( + act('clear_dealer')}> + Clear Dealer ({dealer}) + + ) : game_state !== 1 ? ( + + ) : ( + 'Single Player Mode' + )} + + + {is_dealer ? ( + + act('setup_action', { + action: 'change_grid_size', + data: { new_grid: value }, + }) + } + /> + ) : ( + grid_size + )} + + + {is_dealer ? ( + + act('setup_action', { + action: 'change_mine_count', + data: { new_mines: value }, + }) + } + /> + ) : ( + mine_count + )} + + {!!is_dealer && ( + + + + + act('setup_action', { action: 'auto_place_mines' }) + } + > + Place All + + + + + act('setup_action', { action: 'clear_all_mines' }) + } + > + Clear All + + + {(!placed_mines || !Object.keys(placed_mines).length) && ( + + + act('setup_action', { + action: 'auto_place_mines_self', + }) + } + > + Play Alone + + + )} + + + )} + + + + + + + {`Remaining Mines: `} + + + {remainingMines} + + + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/VoreSweeper/constants.ts b/tgui/packages/tgui/interfaces/VoreSweeper/constants.ts new file mode 100644 index 0000000000..e9fda09390 --- /dev/null +++ b/tgui/packages/tgui/interfaces/VoreSweeper/constants.ts @@ -0,0 +1,25 @@ +export const gameStateToIcon = [ + 'gear', + 'face-grimace', + 'skull-crossbones', + 'face-grin-stars', +] as const; + +export const gameStateToColor = ['gray', 'yellow', 'red', 'green'] as const; + +export const numberToColor = [ + undefined, + '#85ceff', + '#12ff12', + '#ff0000', + '#ff00ff', + '#ff7b00', + 'cyan', + 'black', + 'white', +] as const; + +export const gameTooltip = + 'A two player or singleplayer game where the goal is to reveal all fields without hitting any mines. ' + + 'Have a friend be the dealer and set the playfield up, set it how you want it, or randomize and then play on your own.' + + 'Left click to reveal a field. Right click to place a flag.'; diff --git a/tgui/packages/tgui/interfaces/VoreSweeper/functions.ts b/tgui/packages/tgui/interfaces/VoreSweeper/functions.ts new file mode 100644 index 0000000000..ac9bf8fd19 --- /dev/null +++ b/tgui/packages/tgui/interfaces/VoreSweeper/functions.ts @@ -0,0 +1,46 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export function checkDisabled( + revealed: boolean, + gameState: number, + isDealer: boolean, +): boolean { + if (isDealer && gameState > 0) { + return true; + } + if (!isDealer && gameState < 1 && gameState > 2) return true; + if (revealed) { + return true; + } + return false; +} + +export function getRemainingMines( + gameState: number, + mineCount: number, + isDealer: boolean, + placedMines: Record | null, + placedFlags: Record, +): number { + if (gameState === 3) { + return 0; + } + + const mineTotal = Object.keys(placedMines || {}).length; + const flagTotal = Object.keys(placedFlags).length; + + return mineCount - (isDealer ? mineTotal : flagTotal); +} + +export function getScoreColor( + remainingMines: number, + game_state: number, +): string | undefined { + if (remainingMines < 0 || (!remainingMines && game_state === 1)) { + return 'red'; + } + if (game_state === 3) { + return 'green'; + } + return undefined; +} diff --git a/tgui/packages/tgui/interfaces/VoreSweeper/index.tsx b/tgui/packages/tgui/interfaces/VoreSweeper/index.tsx new file mode 100644 index 0000000000..5c1de21f51 --- /dev/null +++ b/tgui/packages/tgui/interfaces/VoreSweeper/index.tsx @@ -0,0 +1,31 @@ +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; +import { GameArea } from './GameArea'; +import { PlayerMenu } from './PlayerMenu'; +import type { Data } from './types'; + +export const VoreSweeper = (props) => { + const { data } = useBackend(); + + const { grid_size, is_dealer } = data; + const added_height = grid_size * 64; + const added_width = grid_size * 66; + return ( + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/VoreSweeper/types.ts b/tgui/packages/tgui/interfaces/VoreSweeper/types.ts new file mode 100644 index 0000000000..14f379441c --- /dev/null +++ b/tgui/packages/tgui/interfaces/VoreSweeper/types.ts @@ -0,0 +1,13 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export type Data = { + grid_size: number; + mine_count: number; + max_mines: number; + dealer: string | null; + placed_mines: Record | null; + revealed_fields: Record; + placed_flags: Record; + game_state: number; + is_dealer: BooleanLike; +}; diff --git a/tgui/packages/tgui/styles/interfaces/FourInARow.scss b/tgui/packages/tgui/styles/interfaces/FourInARow.scss new file mode 100644 index 0000000000..7368eefe0a --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/FourInARow.scss @@ -0,0 +1,34 @@ +.FourInARow__Tile { + border: 2px solid gray; + background: black; + box-shadow: 0 0 5px white; + transform: scale(0.75); + + &.winning-tile { + border: 3px solid gold; + box-shadow: 0 0 15px 5px gold; + transform: scale(0.9); + } + + &.placed { + border: 2px solid black; + box-shadow: + inset 0 2px 5px rgba(255, 255, 255, 0.6), + 0 2px 5px rgba(0, 0, 0, 0.5); + } +} + +.TicTacToe__Tile { + border: 2px solid gray; + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + + &.winning-tile { + text-shadow: + 0 0 5px gold, + 0 0 10px gold, + 0 0 15px gold; + } +} diff --git a/tgui/packages/tgui/styles/interfaces/NineMen.scss b/tgui/packages/tgui/styles/interfaces/NineMen.scss new file mode 100644 index 0000000000..1a047a3fdc --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/NineMen.scss @@ -0,0 +1,10 @@ +.NineMen__Tile { + position: absolute; + transform: translate(-50%, -50%) scale(0.9); + line-height: '60px'; + justify-content: 'center'; + display: 'flex'; + width: 5rem; + height: 5rem; + font-size: 2.5rem; +} diff --git a/tgui/packages/tgui/styles/interfaces/RpgDice.scss b/tgui/packages/tgui/styles/interfaces/RpgDice.scss new file mode 100644 index 0000000000..fcd40b6dc8 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/RpgDice.scss @@ -0,0 +1,107 @@ +.RpgDice__dice { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: 100px; + height: 100px; + background-color: #f0f0f0; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + font-family: 'Arial', sans-serif; + font-size: 24px; + font-weight: bold; + color: #333; +} + +.RpgDice__dice.d4 { + background-color: rgb(255, 115, 0); + width: 100px; + height: 100px; + clip-path: polygon(50% 0%, 0% 100%, 100% 100%); +} + +.RpgDice__dice.d6 { + background-color: #ad00c4; + width: 100px; + height: 100px; + border-radius: 10px; + box-shadow: inset 0 0 0 1px #ad00c4; +} + +.RpgDice__dice.d8 { + background-color: #00a6ffcf; + width: 100px; + height: 100px; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); +} + +.RpgDice__dice.d10 { + background-color: #00d9ff; + width: 100px; + height: 100px; + clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%); +} + +.RpgDice__dice.d12 { + background-color: #ffc400; + width: 100px; + height: 100px; + clip-path: polygon(50% 0%, 100% 20%, 80% 80%, 20% 80%, 0% 20%); +} + +.RpgDice__dice.d20 { + background-color: #ff0000; + width: 100px; + height: 100px; + clip-path: polygon( + 50% 0%, + 100% 10%, + 90% 50%, + 90% 50%, + 100% 90%, + 50% 100%, + 0% 90%, + 10% 50%, + 10% 50%, + 0% 10% + ); +} + +.RpgDice__dice.d50 { + background-color: #ff66cc; + width: 100px; + height: 100px; + clip-path: polygon( + 50% 0%, + 80% 20%, + 100% 50%, + 80% 80%, + 50% 100%, + 20% 80%, + 0% 50%, + 20% 20% + ); +} + +.RpgDice__dice.d100 { + background-color: #00ff00; + width: 100px; + height: 100px; + clip-path: polygon( + 50% 0%, + 85% 15%, + 100% 50%, + 85% 85%, + 50% 100%, + 15% 85%, + 0% 50%, + 15% 15% + ); +} + +.RpgDice__dice .Button__content { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 9281131caa..d9f22f04b5 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -58,6 +58,9 @@ @include meta.load-css('./interfaces/Wires.scss'); @include meta.load-css('./interfaces/PlushEditor.scss'); @include meta.load-css('./interfaces/ColorMatrixEditor.scss'); +@include meta.load-css('./interfaces/FourInARow.scss'); +@include meta.load-css('./interfaces/RpgDice.scss'); +@include meta.load-css('./interfaces/NineMen.scss'); // Layouts @include meta.load-css('./layouts/Layout.scss'); diff --git a/vorestation.dme b/vorestation.dme index 1f954d55c5..49bd8b7c3d 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -60,6 +60,7 @@ #include "code\__defines\belly_messages.dm" #include "code\__defines\belly_modes_vr.dm" #include "code\__defines\blueprints.dm" +#include "code\__defines\board_game.dm" #include "code\__defines\borer.dm" #include "code\__defines\borg_overlays.dm" #include "code\__defines\callbacks.dm" @@ -2400,6 +2401,15 @@ #include "code\modules\casino\premium_vendors.dm" #include "code\modules\casino\slots.dm" #include "code\modules\casino\spawnergrenade_casino.dm" +#include "code\modules\casino\board_games\board_table.dm" +#include "code\modules\casino\board_games\checkers.dm" +#include "code\modules\casino\board_games\chess.dm" +#include "code\modules\casino\board_games\four_in_a_row.dm" +#include "code\modules\casino\board_games\nine_mens.dm" +#include "code\modules\casino\board_games\rpg_dice.dm" +#include "code\modules\casino\board_games\space_battle.dm" +#include "code\modules\casino\board_games\tic_tac_toe.dm" +#include "code\modules\casino\board_games\vore_sweeper.dm" #include "code\modules\catalogue\atoms.dm" #include "code\modules\catalogue\catalogue_data.dm" #include "code\modules\catalogue\catalogue_data_vr.dm" @@ -4139,6 +4149,7 @@ #include "code\modules\pda\cart_apps.dm" #include "code\modules\pda\cart_vr.dm" #include "code\modules\pda\core_apps.dm" +#include "code\modules\pda\game_launcher.dm" #include "code\modules\pda\messenger.dm" #include "code\modules\pda\messenger_plugins.dm" #include "code\modules\pda\nerdle.dm" @@ -4735,6 +4746,7 @@ #include "code\modules\tgui\modules\ntos-only\uav.dm" #include "code\modules\tgui\states\admin.dm" #include "code\modules\tgui\states\always.dm" +#include "code\modules\tgui\states\board_game.dm" #include "code\modules\tgui\states\conscious.dm" #include "code\modules\tgui\states\contained.dm" #include "code\modules\tgui\states\deep_inventory.dm"