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 37b7b420d8..bf49dbd1b8 100644 Binary files a/icons/obj/casino.dmi and b/icons/obj/casino.dmi differ diff --git a/icons/obj/tables.dmi b/icons/obj/tables.dmi index 05a486a08f..8555d99c51 100644 Binary files a/icons/obj/tables.dmi and b/icons/obj/tables.dmi differ 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"