game collection (#19179)

* game collection

* no phased usage

* .

* spellcheck

* chesskers

* fixes runtime during del

---------

Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
Kashargul
2026-02-21 16:02:32 -05:00
committed by GitHub
co-authored by Cameron Lennox
parent db0636c8d1
commit 0fc8ccc50e
72 changed files with 6248 additions and 47 deletions
+10
View File
@@ -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"
@@ -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
+523
View File
@@ -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
+566
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+15 -8
View File
@@ -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)
+3 -3
View File
@@ -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)
+8 -8
View File
@@ -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)
+3 -3
View File
@@ -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)
+5 -5
View File
@@ -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)
+125
View File
@@ -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)
. = ..()
+1 -1
View File
@@ -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?
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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
+6 -5
View File
@@ -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)
+1 -1
View File
@@ -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))
+7 -7
View File
@@ -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")
+26
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@@ -0,0 +1,18 @@
import { Icon, Tooltip } from 'tgui-core/components';
export const CastlingUsed = (props: { used: boolean }) => {
const { used } = props;
return (
<Tooltip content={`Castling ${used ? '' : 'un'}used`}>
{used ? (
<Icon.Stack>
<Icon name="dungeon" />
<Icon name="ban" color="red" />
</Icon.Stack>
) : (
<Icon name="dungeon" />
)}
</Tooltip>
);
};
@@ -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<Data>();
const {
game_type,
game_state,
current_board,
selected_figure,
valid_moves,
possible_jumps,
} = data;
const [promotionFloating, setPromotionFloating] =
useState<React.JSX.Element | null>(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(
<PromotionSelection
x={x}
y={y}
iconText={piece}
onPromotionFloating={setPromotionFloating}
/>,
);
return;
}
}
act('game_action', {
action: isValidMove ? 'move_figure' : 'select_figure',
data: { loc_x: x, loc_y: y },
});
}
return (
<Section
fill
title="Game Grid"
onContextMenu={(event) => {
event.preventDefault();
}}
>
<Stack>
<Stack.Item grow> {game_state === 2 && promotionFloating}</Stack.Item>
<Stack.Item>
<Stack vertical>
<Stack.Item>
{Array.from({ length: 9 }, (_, row) => (
<Stack key={row}>
{Array.from({ length: 9 }, (_, col) => {
if (col === 0 && row === 0) {
return (
<Stack.Item key="corner">
<Box width={1} />
</Stack.Item>
);
} else if (col === 0 && row > 0) {
return (
<Stack.Item align="center" key={`row-${row}`}>
<Box width={1}>{numToLetter[row - 1]}</Box>
</Stack.Item>
);
} else if (row === 0 && col > 0) {
return (
<Stack.Item textAlign="center" key={`col-${col}`}>
<Box width={5}>{col}</Box>
</Stack.Item>
);
}
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 (
<Stack.Item key={`${x}${y}`}>
<Button
disabled={
checkDisabled(game_state) || !!promotionFloating
}
selected={
selected_figure.length &&
selected_figure[0] === x &&
selected_figure[1] === y
}
color={
isValidMove
? 'teal'
: canJump
? 'yellow'
: (boardX + boardY) % 2 === 0
? 'light-grey'
: 'gray'
}
onClick={() => handleClick(isValidMove, x, y)}
onContextMenu={(event) => {
event.preventDefault();
}}
width={5}
height={5}
fontSize={2.5}
innerStyle={{
color: iconText?.includes('b') ? 'black' : 'white',
display: 'flex',
justifyContent: 'center',
lineHeight: '60px',
}}
>
{getGameIcons(game_type, iconText)}
</Button>
</Stack.Item>
);
})}
</Stack>
))}
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow> {game_state === 1 && promotionFloating}</Stack.Item>
</Stack>
</Section>
);
};
@@ -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<Data>();
const {
game_type,
game_state,
player_one,
player_two,
player_one_time,
player_two_time,
has_won,
game_flags = 0,
winner,
} = data;
return (
<Section
title="Game Status"
buttons={
<Stack>
{(game_state > 0 && (
<Stack.Item>
<Button.Confirm
onClick={() => {
act('clear_game');
}}
>
Clear Game
</Button.Confirm>
</Stack.Item>
)) ||
(game_state === 0 && !!player_one && !!player_two && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('start_game');
}}
>
Start Game
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('swap_players');
}}
>
Swap Players
</Button.Confirm>
</Stack.Item>
</>
))}
{!!player_one && !!player_two && game_state === 3 && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again');
}}
>
Play Again
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again_swapped');
}}
>
Play Again (Swapped Sides)
</Button.Confirm>
</Stack.Item>
</>
)}
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip
tooltip={gameTooltip[game_type]}
displayText="?"
/>
</Stack.Item>
</Stack>
}
>
<Stack fill>
<PlayerPanel
gameState={game_state}
player={player_one}
num={1}
time={player_one_time}
castlingUsed={!!(game_flags & 4)}
/>
<Stack.Item basis="33%">
<Stack vertical textAlign="center">
<Stack.Item>
<Box color="label">Status:</Box>
</Stack.Item>
<Stack.Item>
<Box color={stateToColor(game_state, game_flags, !!has_won)}>
{gameStateToText(game_state, game_flags, winner)}
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
<PlayerPanel
gameState={game_state}
player={player_two}
num={2}
time={player_two_time}
castlingUsed={!!(game_flags & 8)}
/>
</Stack>
</Section>
);
};
@@ -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<Data>();
const { game_type } = data;
const { gameState, player, time, castlingUsed, num } = props;
const player_string = numToText[num - 1];
return (
<Stack.Item basis="33%">
<LabeledList>
<LabeledList.Item label={`Player ${num}`}>
<Button.Confirm
disabled={gameState !== 0}
onClick={() => act(`be_player_${player_string}`)}
>
{player || `Be player ${num}`}
</Button.Confirm>
</LabeledList.Item>
<LabeledList.Item label="Color">
<ColorBox
color={num === 1 ? 'white' : 'black'}
style={{
border: '1px solid white',
boxSizing: 'border-box',
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Time">{formatTime(time)}</LabeledList.Item>
{game_type === 'chess' && (
<LabeledList.Item label="Castling">
<CastlingUsed used={castlingUsed} />
</LabeledList.Item>
)}
</LabeledList>
</Stack.Item>
);
};
@@ -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<React.JSX.Element | null>
>;
}) => {
const { act, data } = useBackend<Data>();
const { x, y, iconText, onPromotionFloating } = props;
return (
<Stack fill vertical align="center" justify="center">
{possiblePromotions.map((option) => (
<Stack.Item key={option}>
<Button
onClick={() => {
act('game_action', {
action: 'move_figure',
data: { loc_x: x, loc_y: y, promotion_choice: option },
});
onPromotionFloating(null);
}}
onContextMenu={(event) => {
event.preventDefault();
}}
width={5}
height={5}
fontSize={2.5}
innerStyle={{
color: iconText?.includes('b') ? 'black' : 'white',
display: 'flex',
justifyContent: 'center',
lineHeight: '60px',
}}
>
{getGameIcons('chess', iconText[0] + option)}
</Button>
</Stack.Item>
))}
</Stack>
);
};
@@ -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.",
};
@@ -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;
}
@@ -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 (
<Window width={800} height={735}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<PlayerMenu />
</Stack.Item>
<Stack.Item grow>
<GameArea />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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[][];
};
@@ -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<Data>();
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 <Icon name="x" color={player_one_color} />;
}
if (placed_chips_ptwo[key]) {
return <Icon name="o" color={player_two_color} />;
}
return null;
}
function isWinningTile(key: string): boolean {
return game_state === 3 && winning_tiles?.includes(key);
}
return (
<Section
fill
title="Game Grid"
onContextMenu={(event) => {
event.preventDefault();
}}
>
<Stack justify="center">
<Stack.Item>
<Stack backgroundColor="#474747" vertical>
<Stack.Item>
{Array.from({ length: grid_y_size }, (_, row) => (
<Stack key={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 (
<Stack.Item key={key}>
<Button
circular={!isTicTacToe}
disabled={
placed_chips_pone[key] ||
placed_chips_ptwo[key] ||
game_state === 3
}
color={isTicTacToe ? undefined : playerColor(key)}
onClick={(_) => {
act('game_action', {
action: 'place_chip',
data: { loc_x: x, loc_y: y },
});
}}
onContextMenu={(event) => {
event.preventDefault();
}}
width={5}
height={5}
className={classes(tileClasses)}
style={isTicTacToe ? undefined : dynamicStyle}
>
{playerIcon(key)}
</Button>
</Stack.Item>
);
})}
</Stack>
))}
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -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<Data>();
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 (
<Section
title="Game Status"
buttons={
<Stack>
{(game_state > 0 && (
<Stack.Item>
<Button.Confirm
onClick={() => {
act('clear_game');
}}
>
Clear Game
</Button.Confirm>
</Stack.Item>
)) ||
(game_state === 0 && player_one && player_two && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('start_game');
}}
>
Start Game
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('swap_players');
}}
>
Swap Players
</Button.Confirm>
</Stack.Item>
</>
))}
{!!player_one && !!player_two && game_state === 3 && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again');
}}
>
Play Again
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again_swapped');
}}
>
Play Again (Swapped Sides)
</Button.Confirm>
</Stack.Item>
</>
)}
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip
tooltip={gameTooltip[isFourRow ? 0 : 1]}
displayText="?"
/>
</Stack.Item>
</Stack>
}
>
<Stack fill>
<PlayerPanel
ticTacToe={isTicTacToe}
gameState={game_state}
player={player_one}
color={player_one_color}
usableColors={colors.filter((c) => c !== player_two_color)}
num={1}
/>
<Stack.Item basis="33%">
<Stack vertical textAlign="center">
<Stack.Item>
<Stack justify="center">
<Stack.Item>
<Box color="label">Size:</Box>
</Stack.Item>
<Stack.Item>
{game_state === 0 && isFourRow ? (
<NumberInput
width="30px"
minValue={5}
maxValue={10}
stepPixelSize={40}
value={grid_x_size}
onChange={(value) => act('change_size', { size: value })}
/>
) : (
grid_x_size
)}
</Stack.Item>
<Stack.Item basis="10%" />
<Stack.Item>
<Box color="label">Win Con.:</Box>
</Stack.Item>
<Stack.Item>
{game_state === 0 && isFourRow ? (
<NumberInput
minValue={4}
maxValue={5}
stepPixelSize={100}
value={win_count}
onChange={(value) => act('change_win', { count: value })}
/>
) : (
win_count
)}
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Box color="label">Status:</Box>
</Stack.Item>
<Stack.Item>
<Box
color={stateToColor(
game_state,
player_one_color,
player_two_color,
!!has_won,
)}
>
{gameStateToText(game_state, winner)}
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
<PlayerPanel
ticTacToe={isTicTacToe}
gameState={game_state}
player={player_two}
color={player_two_color}
usableColors={colors.filter((c) => c !== player_one_color)}
num={2}
/>
</Stack>
</Section>
);
};
@@ -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 (
<Stack.Item basis="33%">
<LabeledList>
<LabeledList.Item label={`Player ${num}`}>
<Button.Confirm
disabled={gameState !== 0}
onClick={() => act(`be_player_${player_string}`)}
>
{player || `Be player ${num}`}
</Button.Confirm>
</LabeledList.Item>
<LabeledList.Item label={'Color'}>
<Stack align="center">
<Stack.Item>
<Dropdown
width="100px"
disabled={gameState !== 0 || !player}
onSelected={(value) =>
act(`set_color_${player_string}`, { color: value })
}
options={usableColors}
selected={color}
/>
</Stack.Item>
<Stack.Item>
{ticTacToe ? (
<Icon color={color} name={num === 1 ? 'x' : 'o'} />
) : (
<ColorBox
color={color}
style={{
border: '1px solid white',
boxSizing: 'border-box',
}}
/>
)}
</Stack.Item>
</Stack>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
);
};
@@ -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.',
];
@@ -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`;
}
@@ -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<Data>();
const { grid_y_size } = data;
const added_height = grid_y_size * 63;
return (
<Window width={685} height={200 + added_height}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<PlayerMenu />
</Stack.Item>
<Stack.Item grow>
<GameArea />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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<string, BooleanLike>;
placed_chips_ptwo: Record<string, BooleanLike>;
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[];
};
@@ -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<Data>();
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 (
<Section fill title="Nine Men's Morris">
<Box position="relative" width="400px" height="400px" mt="50px" mx="auto">
<svg
viewBox="0 0 100 100"
width="100%"
height="100%"
style={{ position: 'absolute', top: 0, left: 0 }}
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
>
<rect x="1" y="1" width="98" height="98" />
<rect x="17" y="17" width="66" height="66" />
<rect x="33" y="33" width="34" height="34" />
<line x1="1" y1="50" x2="33" y2="50" />
<line x1="67" y1="50" x2="99" y2="50" />
<line x1="50" y1="1" x2="50" y2="33" />
<line x1="50" y1="67" x2="50" y2="99" />
</svg>
{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 (
<Button
className="NineMen__Tile"
key={node}
circular
left={`${pos.x}%`}
top={`${pos.y}%`}
selected={isSelected}
color={isValidMove ? 'teal' : isValidRemove ? 'yellow' : 'grey'}
onClick={() => handleClick(isValidMove, node)}
disabled={checkDisabled(game_state)}
innerStyle={{
color: piece === 'b' ? 'black' : 'white',
}}
>
{piece ? textToNine[piece] : ''}
</Button>
);
})}
</Box>
</Section>
);
};
@@ -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<Data>();
const {
game_state,
phase,
player_one,
player_two,
player_one_time,
player_two_time,
has_won,
winner,
pone_pieces,
ptwo_pieces,
} = data;
return (
<Section
title="Game Status"
buttons={
<Stack>
{(game_state > 0 && (
<Stack.Item>
<Button.Confirm
onClick={() => {
act('clear_game');
}}
>
Clear Game
</Button.Confirm>
</Stack.Item>
)) ||
(game_state === 0 && !!player_one && !!player_two && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('start_game');
}}
>
Start Game
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('swap_players');
}}
>
Swap Players
</Button.Confirm>
</Stack.Item>
</>
))}
{!!player_one && !!player_two && game_state === 3 && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again');
}}
>
Play Again
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again_swapped');
}}
>
Play Again (Swapped Sides)
</Button.Confirm>
</Stack.Item>
</>
)}
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
</Stack.Item>
</Stack>
}
>
<Stack fill>
<PlayerPanel
gameState={game_state}
player={player_one}
num={1}
time={player_one_time}
pieces={pone_pieces}
/>
<Stack.Item basis="33%">
<Stack vertical textAlign="center">
<Stack.Item>
<Box color="label">Status:</Box>
</Stack.Item>
<Stack.Item>
<Box color={stateToColor(game_state, !!has_won)}>
{gameStateToText(game_state, winner)}
</Box>
</Stack.Item>
<Stack.Item>{phastToText[phase]}</Stack.Item>
</Stack>
</Stack.Item>
<PlayerPanel
gameState={game_state}
player={player_two}
num={2}
time={player_two_time}
pieces={ptwo_pieces}
/>
</Stack>
</Section>
);
};
@@ -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<Data>();
const { gameState, player, pieces, time, num } = props;
const player_string = numToText[num - 1];
return (
<Stack.Item basis="33%">
<LabeledList>
<LabeledList.Item label={`Player ${num}`}>
<Button.Confirm
disabled={gameState !== 0}
onClick={() => act(`be_player_${player_string}`)}
>
{player || `Be player ${num}`}
</Button.Confirm>
</LabeledList.Item>
<LabeledList.Item label="Color">
<ColorBox
color={num === 1 ? 'white' : 'black'}
style={{
border: '1px solid white',
boxSizing: 'border-box',
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Not Placed">{pieces}</LabeledList.Item>
<LabeledList.Item label="Time">{formatTime(time)}</LabeledList.Item>
</LabeledList>
</Stack.Item>
);
};
@@ -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 },
};
@@ -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;
}
@@ -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 (
<Window width={800} height={735}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<PlayerMenu />
</Stack.Item>
<Stack.Item grow>
<GameArea />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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;
};
@@ -0,0 +1,49 @@
import { useBackend } from 'tgui/backend';
import { Box, Button, LabeledList, Section, Stack } from 'tgui-core/components';
type Data = {
available_games: Record<string, string | null>;
};
export const pda_game_launcher = (props) => {
const { act, data } = useBackend<Data>();
const { available_games } = data;
return (
<Box>
<Section title="Game Launcher V1.0">
<Stack vertical fill>
<Stack.Item>
Play your favourite games everywhere, any time! Don't get caught!
</Stack.Item>
<LabeledList>
{Object.keys(available_games).map((game) => {
const gameAction = available_games[game];
return (
<LabeledList.Item key={game} label={game}>
<Stack>
<Stack.Item>
<Button onClick={() => act(game)}>Play {game}</Button>
</Stack.Item>
{!!gameAction && (
<Stack.Item>
<Button.Confirm
onClick={() => act(game, { close: true })}
icon="circle-xmark"
iconColor="red"
color="transparent"
tooltip="forcefully close the game"
/>
</Stack.Item>
)}
</Stack>
</LabeledList.Item>
);
})}
</LabeledList>
</Stack>
</Section>
</Box>
);
};
@@ -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<Data>();
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<string[]>([]);
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const inputRef: RefObject<HTMLInputElement | null> = 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<HTMLInputElement>): 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 (
<Section
title="Dice Selection"
fill
buttons={
<Stack>
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
</Stack.Item>
</Stack>
}
>
<Stack vertical fill>
<Stack.Item>
<Stack>
{firstHalf.map((dice, index) => (
<Stack.Item grow key={index}>
<Button
onClick={() => act('roll_dice', { dice_size: dice })}
className={classes(['RpgDice__dice', `d${dice}`, 'side'])}
>
{dice}
</Button>
</Stack.Item>
))}
</Stack>
</Stack.Item>
<Stack.Item>
<Stack textAlign="center">
{secondHalf.map((dice, index) => (
<Stack.Item grow key={index}>
<Button
onClick={() => act('roll_dice', { dice_size: dice })}
className={classes(['RpgDice__dice', `d${dice}`, 'side'])}
>
{dice}
</Button>
</Stack.Item>
))}
</Stack>
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<Stack>
<Stack.Item grow>
<LabeledList>
<LabeledList.Item label="Size">
<NumberInput
width="50px"
minValue={1}
maxValue={10000}
value={diceSize}
onChange={setDiceSize}
/>
</LabeledList.Item>
<LabeledList.Item label="Count">
<NumberInput
width="50px"
stepPixelSize={40}
minValue={1}
maxValue={10}
value={diceCount}
onChange={setDiceCount}
/>
</LabeledList.Item>
<LabeledList.Item label="Modifier">
<Stack>
<Stack.Item>
<NumberInput
width="50px"
value={modifier}
onChange={setModifier}
/>
<Stack.Item inline>
<Button.Checkbox
checked={applyToAll}
onClick={() => setApplyToAll(!applyToAll)}
>
Apply to all
</Button.Checkbox>
</Stack.Item>
</Stack.Item>
</Stack>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item>
<Button
onClick={() =>
act('roll_dice', {
dice_size: diceSize,
dice_count: diceCount,
dice_mod: modifier,
mod_all: applyToAll,
})
}
icon="dice"
iconSize={2}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{/** Requires rust g dice
<Stack.Divider />
<Stack.Item>
<Stack>
<Stack.Item>
<Button
disabled
onClick={handleUndo}
icon="rotate-left"
iconSize={2}
/>
</Stack.Item>
<Stack.Item grow>
<Button
disabled
onClick={handleRedo}
icon="rotate-right"
iconSize={2}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled
onClick={() => {
updateHistory(customInput);
act('custom_roll', { custom: customInput });
setCustomInput('');
requestAnimationFrame(() => inputRef.current?.focus());
}}
iconSize={2}
icon="paper-plane"
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack fill>
<Stack.Item grow>
<Input
disabled
fluid
ref={inputRef}
onEnter={(value) => {
updateHistory(value);
act('custom_roll', { custom: value });
setCustomInput('');
requestAnimationFrame(() => inputRef.current?.focus());
}}
maxLength={200}
value={customInput}
onChange={setCustomInput}
onKeyDown={handleKeyDown}
/>
</Stack.Item>
</Stack>
</Stack.Item>
*/}
</Stack>
</Section>
);
};
@@ -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<Data>();
const { last_rolls } = data;
const messagesEndRef: RefObject<HTMLDivElement | null> = 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 (
<Section
title="Roll history"
ref={messagesEndRef}
scrollable
fill
buttons={
<Button.Confirm onBlur={() => act('clear_history')} icon="trash" />
}
>
<Stack vertical fill>
{last_rolls.map((roll, index) => (
<Stack.Item key={index}>
<Box inline color="label">
{roll.player}:
</Box>
<Box inline bold>
{roll.count}
</Box>
{!!roll.size && (
<Box inline bold>
d{roll.size}
</Box>
)}
{!!roll.mod && (
<Box bold inline>
+{roll.mod}
</Box>
)}
<Box inline preserveWhitespace>
{`: `}
</Box>
{!!roll.results && (
<DetailedResults
results={roll.results}
mod={roll.mod || 0}
applyToAll={!!roll.apply_to_all}
/>
)}
{(!roll.results || roll.results.length > 1 || !!roll.mod) && (
<Box inline bold>
{roll.sum}
</Box>
)}
</Stack.Item>
))}
</Stack>
</Section>
);
};
export const DetailedResults = (props: {
results: Result[];
mod: number;
applyToAll: boolean;
}) => {
const { results, mod, applyToAll } = props;
const moreThanOne = results.length > 1;
return (
<>
{!!mod && !applyToAll && moreThanOne && <Box inline>{`(`}</Box>}
{results.map((result, index) => (
<Fragment key={index}>
{!!mod && applyToAll && moreThanOne && <Box inline>{`(`}</Box>}
<Box
inline
color={stateToColor[result.state]}
bold={!!stateToColor[result.state] || !moreThanOne}
>
{result.result}
</Box>
<Box inline preserveWhitespace>
{!!mod && applyToAll && ` + ${mod}`}
{!!mod && applyToAll && !!moreThanOne && `)`}
</Box>
{(moreThanOne || !!mod) &&
(index < results.length - 1 ? (
<Box inline preserveWhitespace>{` + `}</Box>
) : (
<Box inline preserveWhitespace>
{!!mod && !applyToAll && !!moreThanOne && `)`}
{!!mod && !applyToAll && ` + ${mod}`}
{` = `}
</Box>
))}
</Fragment>
))}
</>
);
};
@@ -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;
@@ -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 (
<Window width={900} height={450}>
<Window.Content>
<Stack fill>
<Stack.Item basis="33%">
<PlayerMenu />
</Stack.Item>
<Stack.Item grow mb="5px">
<RollArea />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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 };
@@ -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<Data>();
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 (
<Section
fill
title="Game Grid"
onContextMenu={(event) => {
event.preventDefault();
}}
>
<Stack>
<Stack.Item>
<Playfield
player={1}
isOponent={current_player === player_two}
isSelf={current_player === player_one}
shotsFired={shots_fired_ptwo}
destroyedShips={destroyed_ships_ptwo}
shipBeingPlaced={playerOnePlaceShip}
orientation={playerOneOrientation}
shipCount={ship_count_pone}
/>
</Stack.Item>
<Stack.Item minWidth="20px" />
<Stack.Item>
<Playfield
player={2}
isOponent={current_player === player_one}
isSelf={current_player === player_two}
shotsFired={shots_fired_pone}
destroyedShips={destroyed_ships_pone}
shipBeingPlaced={playerTwoPlaceShip}
orientation={playerTwoOrientation}
shipCount={ship_count_ptwo}
/>
</Stack.Item>
</Stack>
</Section>
);
};
const Playfield = (props: {
player: number;
isOponent: boolean;
isSelf: boolean;
shotsFired: Record<string, BooleanLike>;
destroyedShips: Ship[];
shipBeingPlaced: string;
orientation: 'vertical' | 'horizontal';
shipCount: Record<string, number>;
}) => {
const { data, act } = useBackend<Data>();
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 (
<Stack backgroundColor="#474747" vertical>
<Stack.Item>
{Array.from({ length: 11 }, (_, row) => (
<Stack key={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 (
<Stack.Item key={key} align="center" textAlign="center">
{col === 0 && row === 0 ? (
<Box width={1} />
) : col === 0 && row > 0 ? (
<Box width={1}>{row - 1}</Box>
) : row === 0 && col > 0 ? (
<Box width={5}>{numToLetter[col - 1]}</Box>
) : (
<div onMouseEnter={() => handleButtonHover(x, y)}>
<Button
disabled={
((game_state === 2 && isOponent) ||
isSelf ||
game_state === 4) &&
!(isSelf && isOponent)
}
onClick={(_) => {
if (game_state === 1) {
handlePlacement(x, y);
return;
}
act('game_action', {
action: 'fire_shot',
data: { loc_x: x, loc_y: y, player: player },
});
}}
onContextMenu={(event) => {
event.preventDefault();
if (game_state === 1) {
act('remove_ship', {
loc_x: x,
loc_y: y,
player: player,
});
}
}}
style={{
backgroundColor: isHighlighted
? invalidCells
? 'rgba(255, 0, 0, 0.5)'
: 'rgba(76, 175, 80, 0.5)'
: getCellStyle(x, y).backgroundColor,
border: getCellStyle(x, y).border,
}}
width={5}
height={5}
/>
</div>
)}
</Stack.Item>
);
})}
</Stack>
))}
</Stack.Item>
</Stack>
);
};
@@ -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<React.SetStateAction<string>>;
playerTwoPlaceShip: string;
onPlayerTwoPlaceShip: React.Dispatch<React.SetStateAction<string>>;
playerOneOrientation: 'horizontal' | 'vertical';
onPlayerOneOrientation: React.Dispatch<
React.SetStateAction<'horizontal' | 'vertical'>
>;
playerTwoOrientation: 'horizontal' | 'vertical';
onPlayerTwoOrientation: React.Dispatch<
React.SetStateAction<'horizontal' | 'vertical'>
>;
}) => {
const { act, data } = useBackend<Data>();
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 (
<Section
title="Game Status"
buttons={
<Stack>
{(game_state > 0 && (
<Stack.Item>
<Button.Confirm
onClick={() => {
act('clear_game');
}}
>
Clear Game
</Button.Confirm>
</Stack.Item>
)) ||
(game_state === 0 && !!player_one && !!player_two && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('prepare_game');
}}
>
Prepare Game
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('swap_players');
}}
>
Swap Players
</Button.Confirm>
</Stack.Item>
</>
))}
{game_state === 1 && !!player_one && !!player_two && !!all_placed && (
<Stack.Item>
<Button.Confirm
onClick={() => {
act('start_game');
}}
>
Start Game
</Button.Confirm>
</Stack.Item>
)}
{!!player_one && !!player_two && game_state === 4 && (
<>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again');
}}
>
Play Again
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => {
act('play_again_swapped');
}}
>
Play Again (Swapped Sides)
</Button.Confirm>
</Stack.Item>
</>
)}
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
</Stack.Item>
</Stack>
}
>
<Stack fill>
<PlayerPanel
gameState={game_state}
player={player_one}
num={1}
availableShips={ship_sizes}
shipCount={ship_count_pone}
playerPlaceShip={playerOnePlaceShip}
onPlayerPlaceShip={onPlayerOnePlaceShip}
playerRotation={playerOneOrientation}
onPlayerRotation={onPlayerOneOrientation}
/>
<Stack.Item basis="33%">
<Stack vertical textAlign="center">
<Stack.Item>
<Box color="label">Status:</Box>
</Stack.Item>
<Stack.Item>
<Box color={stateToColor(game_state, !!has_won)}>
{gameStateToText[game_state] ?? `${winner} Won`}
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
<PlayerPanel
gameState={game_state}
player={player_two}
num={2}
availableShips={ship_sizes}
shipCount={ship_count_ptwo}
playerPlaceShip={playerTwoPlaceShip}
onPlayerPlaceShip={onPlayerTwoPlaceShip}
playerRotation={playerTwoOrientation}
onPlayerRotation={onPlayerTwoOrientation}
/>
</Stack>
</Section>
);
};
@@ -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<string, number>;
shipCount: Record<string, number>;
playerPlaceShip: string;
onPlayerPlaceShip: React.Dispatch<React.SetStateAction<string>>;
playerRotation: 'horizontal' | 'vertical';
onPlayerRotation: React.Dispatch<
React.SetStateAction<'horizontal' | 'vertical'>
>;
}) => {
const { data, act } = useBackend<Data>();
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 (
<Stack.Item basis="33%">
<LabeledList>
<LabeledList.Item label={`Player ${num}`}>
<Stack>
<Stack.Item grow>
<Button.Confirm
disabled={gameState !== 0}
onClick={() => act(`be_player_${player_string}`)}
>
{player || `Be player ${num}`}
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button
icon="arrow-rotate-right"
onClick={() =>
onPlayerRotation(
playerRotation === 'horizontal' ? 'vertical' : 'horizontal',
)
}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label={`Remaining Ships`}>
<Stack vertical>
{Object.keys(availableShips).map((ship) => {
const remaining = shipCount[ship] || 0;
return (
<Stack.Item key={ship}>
<Button.Checkbox
checked={playerPlaceShip === ship}
onClick={() => onPlayerPlaceShip(ship)}
disabled={player !== current_player}
>
<Box inline>
{ship}: {remaining} - Size: {availableShips[ship]}
</Box>
</Button.Checkbox>
</Stack.Item>
);
})}
</Stack>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
);
};
@@ -0,0 +1,29 @@
export const numToText = ['one', 'two'] as const;
export const numToLetter = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
] as const;
export const gameStateToText = [
'Setup',
'Ship Placement',
'Player 1',
'Player 2',
undefined,
] as const;
export const gameTooltip =
"A two player game where the goal is to sink the opponent's ships. To start the game, join with a friend and switch to the setup phase. " +
'Once there, each player places their ships. As soon as both players have placed their ships, you can start playing. ' +
"The goal is to sink the opponent's ships by hitting them during your turn. You can place your ships by clicking onto the playfield. " +
'The current position is shown as a preview. Left click places a ship. You can rotate it with a middle click or remove it with a right click. ' +
"To switch the ship type, use the mousewheel. Click on your opponent's field during your turn to fire at the selected location.";
@@ -0,0 +1,40 @@
export function stateToColor(
gameState: number,
isWinner: boolean,
): string | undefined {
if (gameState < 4) {
return undefined;
}
return isWinner ? 'green' : 'red';
}
export function generateShipCoordinates(
x: number,
y: number,
size: number,
orientation: 'horizontal' | 'vertical',
) {
const coords: [number, number][] = [];
for (let i = 0; i < size; i++) {
const newX = orientation === 'horizontal' ? x + i : x;
const newY = orientation === 'vertical' ? y + i : y;
coords.push([newX, newY]);
}
return coords;
}
export function getNextAvailableShip(
currentShip: string,
remainingShips: Record<string, number>,
) {
const availableShipNames = Object.keys(remainingShips);
const currentShipIndex = availableShipNames.indexOf(currentShip);
for (let i = currentShipIndex + 1; i < availableShipNames.length; i++) {
const nextShip = availableShipNames[i];
if (remainingShips[nextShip] > 0) {
return nextShip;
}
}
return null;
}
@@ -0,0 +1,171 @@
import { useEffect, useState } from 'react';
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 SpaceBattle = (props) => {
const { data } = useBackend<Data>();
const { ship_sizes, game_state, current_player, player_one, player_two } =
data;
const [playerOnePlaceShip, setPlayerOnePlaceShip] = useState<string>(
Object.keys(ship_sizes)[0],
);
const [playerTwoPlaceShip, setPlayerTwoPlaceShip] = useState<string>(
Object.keys(ship_sizes)[0],
);
const [playerOneOrientation, setPlayerOneOrientation] = useState<
'horizontal' | 'vertical'
>('horizontal');
const [playerTwoOrientation, setPlayerTwoOrientation] = useState<
'horizontal' | 'vertical'
>('horizontal');
const availableShips = Object.keys(ship_sizes);
const [mousePos, setMousePos] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const handleMouseMove = (event: MouseEvent) => {
setMousePos({
x: event.clientX,
y: event.clientY,
});
};
const handleMouseDown = (e: MouseEvent) => {
if (e.button === 1) {
if (game_state !== 1) return;
const playerOneGrid = { x1: 0, y1: 0, x2: 650, y2: 910 };
const playerTwoGrid = { x1: 700, y1: 0, x2: 1400, y2: 910 };
const isPlayerOne =
mousePos.x >= playerOneGrid.x1 &&
mousePos.x <= playerOneGrid.x2 &&
mousePos.y >= playerOneGrid.y1 &&
mousePos.y <= playerOneGrid.y2;
const isPlayerTwo =
mousePos.x >= playerTwoGrid.x1 &&
mousePos.x <= playerTwoGrid.x2 &&
mousePos.y >= playerTwoGrid.y1 &&
mousePos.y <= playerTwoGrid.y2;
if (isPlayerOne && current_player === player_one) {
setPlayerOneOrientation((prev) =>
prev === 'horizontal' ? 'vertical' : 'horizontal',
);
}
if (isPlayerTwo && current_player === player_two) {
setPlayerTwoOrientation((prev) =>
prev === 'horizontal' ? 'vertical' : 'horizontal',
);
}
}
};
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mousedown', handleMouseDown);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mousedown', handleMouseDown);
};
}, [mousePos, game_state, current_player, player_one, player_two]);
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (game_state !== 1) return;
const playerOneGrid = { x1: 0, y1: 0, x2: 650, y2: 910 };
const playerTwoGrid = { x1: 700, y1: 0, x2: 1400, y2: 910 };
const isPlayerOne =
mousePos.x >= playerOneGrid.x1 &&
mousePos.x <= playerOneGrid.x2 &&
mousePos.y >= playerOneGrid.y1 &&
mousePos.y <= playerOneGrid.y2;
const isPlayerTwo =
mousePos.x >= playerTwoGrid.x1 &&
mousePos.x <= playerTwoGrid.x2 &&
mousePos.y >= playerTwoGrid.y1 &&
mousePos.y <= playerTwoGrid.y2;
if (isPlayerOne && current_player === player_one) {
const currentShipIndex = availableShips.indexOf(playerOnePlaceShip);
let newShipIndex = currentShipIndex;
if (e.deltaY > 0) {
newShipIndex = (currentShipIndex + 1) % availableShips.length;
} else if (e.deltaY < 0) {
newShipIndex =
(currentShipIndex - 1 + availableShips.length) %
availableShips.length;
}
setPlayerOnePlaceShip(availableShips[newShipIndex]);
}
if (isPlayerTwo && current_player === player_two) {
const currentShipIndex = availableShips.indexOf(playerTwoPlaceShip);
let newShipIndex = currentShipIndex;
if (e.deltaY > 0) {
newShipIndex = (currentShipIndex + 1) % availableShips.length;
} else if (e.deltaY < 0) {
newShipIndex =
(currentShipIndex - 1 + availableShips.length) %
availableShips.length;
}
setPlayerTwoPlaceShip(availableShips[newShipIndex]);
}
};
window.addEventListener('wheel', handleWheel);
return () => {
window.removeEventListener('wheel', handleWheel);
};
}, [
game_state,
current_player,
playerOnePlaceShip,
playerTwoPlaceShip,
availableShips,
mousePos,
]);
return (
<Window width={1400} height={910}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<PlayerMenu
playerOnePlaceShip={playerOnePlaceShip}
onPlayerOnePlaceShip={setPlayerOnePlaceShip}
playerTwoPlaceShip={playerTwoPlaceShip}
onPlayerTwoPlaceShip={setPlayerTwoPlaceShip}
playerOneOrientation={playerOneOrientation}
onPlayerOneOrientation={setPlayerOneOrientation}
playerTwoOrientation={playerTwoOrientation}
onPlayerTwoOrientation={setPlayerTwoOrientation}
/>
</Stack.Item>
<Stack.Item grow>
<GameArea
playerOnePlaceShip={playerOnePlaceShip}
playerTwoPlaceShip={playerTwoPlaceShip}
playerOneOrientation={playerOneOrientation}
playerTwoOrientation={playerTwoOrientation}
/>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,27 @@
import type { BooleanLike } from 'tgui-core/react';
export type Ship = {
name: string;
size: number;
coords?: [number, number][];
player?: number;
};
export type Data = {
current_player: string;
ship_sizes: Record<string, number>;
total_ships: Record<string, number>;
player_one: string;
player_two: string;
ships_have_been_placed: BooleanLike;
shots_fired_pone: Record<string, number>;
shots_fired_ptwo: Record<string, number>;
destroyed_ships_pone: Ship[];
destroyed_ships_ptwo: Ship[];
visible_ships: Ship[];
ship_count_pone: Record<string, number>;
ship_count_ptwo: Record<string, number>;
game_state: number;
winner: string;
has_won: BooleanLike;
};
@@ -0,0 +1,127 @@
import { isNumber } from 'es-toolkit';
import { useEffect, useState } from 'react';
import { useBackend } from 'tgui/backend';
import { Box, Button, Section, Stack } from 'tgui-core/components';
import { numberToColor } from './constants';
import { checkDisabled } from './functions';
import type { Data } from './types';
export const GameArea = (props) => {
const { act, data } = useBackend<Data>();
const [mineHit, setMineHit] = useState('');
const {
grid_size,
revealed_fields,
placed_flags,
game_state,
placed_mines,
is_dealer,
} = data;
useEffect(() => {
if (Object.keys(revealed_fields).length === 0) {
setMineHit('');
}
}, [revealed_fields]);
useEffect(() => {
for (const key in revealed_fields) {
if (revealed_fields[key] === 'M') {
setMineHit(key);
break;
}
}
}, [revealed_fields]);
const getTileContent = (x: number, y: number) => {
const key = `${x},${y}`;
if (revealed_fields[key] !== undefined) {
if (revealed_fields[key] === 'M') {
return '💣';
}
return revealed_fields[key];
}
if (placed_flags[key]) {
if (game_state > 1 && !placed_mines?.[key]) {
return <Box style={{ textDecoration: 'line-through' }}>🚩</Box>;
}
return '🚩';
}
if ((game_state > 1 || is_dealer) && placed_mines && placed_mines[key]) {
return game_state === 3 ? '🚩' : '💣';
}
return '';
};
return (
<Section
fill
title="Game Grid"
onContextMenu={(event) => {
event.preventDefault();
}}
>
<Stack justify="center">
<Stack.Item>
<Stack vertical>
<Stack.Item>
{Array.from({ length: grid_size }, (_, row) => (
<Stack key={row}>
{Array.from({ length: grid_size }, (_, col) => {
const x = col + 1;
const y = row + 1;
const key = `${x},${y}`;
const displayText = getTileContent(x, y);
return (
<Stack.Item key={key}>
<Button
disabled={checkDisabled(
revealed_fields[key] !== undefined,
game_state,
!!is_dealer,
)}
color={mineHit === key && 'red '}
onClick={(_) => {
act(is_dealer ? 'setup_action' : 'game_action', {
action: is_dealer ? 'place_mine' : 'open_field',
data: { loc_x: x, loc_y: y },
});
}}
onContextMenu={(event) => {
event.preventDefault();
act(is_dealer ? 'setup_action' : 'game_action', {
action: is_dealer ? 'remove_mine' : 'toggle_flag',
data: { loc_x: x, loc_y: y },
});
}}
textColor={
isNumber(displayText) && numberToColor[displayText]
}
width={5}
height={5}
fontSize={2.5}
innerStyle={{
display: 'flex',
justifyContent: 'center',
lineHeight: '60px',
}}
>
{!!displayText && displayText}
</Button>
</Stack.Item>
);
})}
</Stack>
))}
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -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<Data>();
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 (
<Section
title="Game Status"
buttons={
<Stack>
<Stack.Item>
{(game_state > 0 && (
<Button.Confirm
onClick={() => {
act('restart_game');
}}
>
Restart Game
</Button.Confirm>
)) ||
(game_state === 0 && dealer && !remainingMines && (
<Button.Confirm
onClick={() => {
act('setup_action', { action: 'start_game' });
}}
>
Start Game
</Button.Confirm>
))}
</Stack.Item>
<Stack.Item>
<Button onClick={() => act('invite_player')}>Invite Player</Button>
</Stack.Item>
<Stack.Item>
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
</Stack.Item>
</Stack>
}
>
<Stack vertical>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Dealer">
{dealer ? (
<Button.Confirm onClick={() => act('clear_dealer')}>
Clear Dealer ({dealer})
</Button.Confirm>
) : game_state !== 1 ? (
<Button onClick={() => act('be_dealer')}>Be Dealer</Button>
) : (
'Single Player Mode'
)}
</LabeledList.Item>
<LabeledList.Item label="Grid Size">
{is_dealer ? (
<NumberInput
width="30px"
value={grid_size}
minValue={4}
maxValue={16}
stepPixelSize={40}
onChange={(value) =>
act('setup_action', {
action: 'change_grid_size',
data: { new_grid: value },
})
}
/>
) : (
grid_size
)}
</LabeledList.Item>
<LabeledList.Item label="Mine Count">
{is_dealer ? (
<NumberInput
width="40px"
value={mine_count}
minValue={
placed_mines ? Object.keys(placed_mines).length || 1 : 1
}
maxValue={max_mines}
stepPixelSize={10}
onChange={(value) =>
act('setup_action', {
action: 'change_mine_count',
data: { new_mines: value },
})
}
/>
) : (
mine_count
)}
</LabeledList.Item>
{!!is_dealer && (
<LabeledList.Item label="Ranomize Mines">
<Stack>
<Stack.Item>
<Button.Confirm
onClick={() =>
act('setup_action', { action: 'auto_place_mines' })
}
>
Place All
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() =>
act('setup_action', { action: 'clear_all_mines' })
}
>
Clear All
</Button.Confirm>
</Stack.Item>
{(!placed_mines || !Object.keys(placed_mines).length) && (
<Stack.Item>
<Button.Confirm
onClick={() =>
act('setup_action', {
action: 'auto_place_mines_self',
})
}
>
Play Alone
</Button.Confirm>
</Stack.Item>
)}
</Stack>
</LabeledList.Item>
)}
</LabeledList>
</Stack.Item>
<Stack.Item>
<Stack fill>
<Stack.Item grow>
<Box preserveWhitespace inline color="label">
{`Remaining Mines: `}
</Box>
<Box
fontSize={2}
inline
color={getScoreColor(remainingMines, game_state)}
>
{remainingMines}
</Box>
</Stack.Item>
<Stack.Item grow>
<Icon
size={2}
name={gameStateToIcon[game_state]}
color={gameStateToColor[game_state]}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -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.';
@@ -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<string, BooleanLike> | null,
placedFlags: Record<string, BooleanLike>,
): 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;
}
@@ -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<Data>();
const { grid_size, is_dealer } = data;
const added_height = grid_size * 64;
const added_width = grid_size * 66;
return (
<Window
width={Math.max(25 + added_width, 350)}
height={(is_dealer ? 260 : 230) + added_height}
>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<PlayerMenu />
</Stack.Item>
<Stack.Item grow>
<GameArea />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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<string, BooleanLike> | null;
revealed_fields: Record<string, number | string>;
placed_flags: Record<string, BooleanLike>;
game_state: number;
is_dealer: BooleanLike;
};
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
+3
View File
@@ -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');
+12
View File
@@ -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"