add: minesweeper PDA game (#26860)

* minesweeper with no style

* less string operations

* review and aylongo tgui changes

* bundle

* minesweeper with no style

* less string operations

* review and aylongo tgui changes

* bundle

* bundle

* bundle

* silent. pda is booming

* codestyle

* review

* revert

* Apply suggestions from code review

Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>
Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com>

* adjusts the name of a proc

---------

Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>
This commit is contained in:
ROdenFL
2024-11-20 19:40:57 +03:00
committed by GitHub
parent f56c5dfbdf
commit 59dd3a645d
10 changed files with 575 additions and 59 deletions
+4 -1
View File
@@ -63,7 +63,10 @@ GLOBAL_LIST_EMPTY(PDAs)
new/datum/data/pda/app/manifest,
new/datum/data/pda/app/nanobank,
new/datum/data/pda/app/atmos_scanner,
new/datum/data/pda/utility/flashlight)
new/datum/data/pda/utility/flashlight,
new/datum/data/pda/app/games,
// Here our games go
new/datum/data/pda/app/game/minesweeper)
var/list/shortcut_cache = list()
var/list/shortcut_cat_order = list()
var/list/notifying_programs = list()
+31
View File
@@ -0,0 +1,31 @@
// Games Hub
/datum/data/pda/app/games
name = "Games"
icon = "gamepad"
template = "pda_games"
/datum/data/pda/app/games/update_ui(mob/user, list/data)
var/list/games = list()
for(var/datum/data/pda/app/game/game in pda.programs)
games += list(game.get_game_info())
data["games"] = games
/datum/data/pda/app/games/ui_act(action, params)
if(..())
return
switch(action)
if("play")
var/datum/data/pda/app/game/game = locate(params["id"]) in pda.programs
pda.start_program(game)
// Game-type App
/datum/data/pda/app/game
name = "base game"
icon = "gamepad"
hidden = TRUE
/datum/data/pda/app/game/proc/get_game_info()
return list(name = name, icon = icon, id = UID())
+240
View File
@@ -0,0 +1,240 @@
// Minesweeper
#define MINESWEEPER_ROWS 16
#define MINESWEEPER_COLUMNS 16
#define MINESWEEPER_BOMBS 40
/datum/data/pda/app/game/minesweeper
name = "Minesweeper"
template = "pda_minesweeper"
update = PDA_APP_NOUPDATE
/// Thing, to make first touch safety
var/first_touch = TRUE
// Win condition things
var/setted_flags = 0
var/flagged_bombs = 0
var/opened_cells = 0
/// Decision to make interface untouchable in the momemnt of regenerating
var/ignore_touches = FALSE
/// Here we have all the minesweeper info
var/list/minesweeper_matrix = list()
// generations vars
var/generation_rows = MINESWEEPER_ROWS
var/generation_columns = MINESWEEPER_COLUMNS
var/generation_bombs = MINESWEEPER_BOMBS
/// The moment then game was started for point count
var/start_time = 0
/// The leaderboard list
var/static/list/leaderboard = list()
/datum/data/pda/app/game/minesweeper/start()
. = ..()
make_empty_matrix()
/datum/data/pda/app/game/minesweeper/update_ui(mob/user, list/data)
data["matrix"] = minesweeper_matrix
data["flags"] = setted_flags
data["bombs"] = generation_bombs
data["leaderboard"] = leaderboard
/datum/data/pda/app/game/minesweeper/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
if(..())
return
if(ignore_touches)
return
switch(action)
if("Square")
switch(params["mode"])
if("bomb")
if(first_touch)
generate_matrix(params["X"], params["Y"])
open_cell(params["X"], params["Y"])
if(minesweeper_matrix[params["X"]][params["Y"]]["bomb"])
on_loose(ui.user)
SStgui.update_uis(pda)
return
if("flag")
if(first_touch || minesweeper_matrix[params["X"]][params["Y"]]["open"])
return
if(minesweeper_matrix[params["X"]][params["Y"]]["flag"])
minesweeper_matrix[params["X"]][params["Y"]]["flag"] = FALSE
setted_flags -= 1
if(minesweeper_matrix[params["X"]][params["Y"]]["bomb"])
flagged_bombs -= 1
else
minesweeper_matrix[params["X"]][params["Y"]]["flag"] = TRUE
setted_flags += 1
if(minesweeper_matrix[params["X"]][params["Y"]]["bomb"])
flagged_bombs += 1
check_win(ui.user)
SStgui.update_uis(pda)
/datum/data/pda/app/game/minesweeper/proc/check_win(mob/user)
if(flagged_bombs == generation_bombs && \
setted_flags == generation_bombs && \
opened_cells == (generation_rows * generation_columns - generation_bombs))
on_win(user)
/datum/data/pda/app/game/minesweeper/proc/on_win(mob/user)
ignore_touches = TRUE
if(!pda.silent)
playsound(get_turf(pda), 'sound/machines/ping.ogg', 20, TRUE)
addtimer(CALLBACK(src, PROC_REF(make_empty_matrix)), 5 SECONDS)
add_into_leaders(user, world.time - start_time)
/datum/data/pda/app/game/minesweeper/proc/add_into_leaders(mob/user, game_time)
var/nickname = tgui_input_text(user, "You finished the game in [game_time / 10] seconds.\n Write a nickname to save your result on the leaderboard.\n", "Minesweeper", "", 10)
if(!nickname)
return
leaderboard += list(list("name" = nickname, "time" = "[game_time/10]"))
/datum/data/pda/app/game/minesweeper/proc/on_loose(mob/user)
ignore_touches = TRUE
if(!pda.silent)
playsound(get_turf(pda), 'sound/effects/explosionfar.ogg', 50, TRUE)
addtimer(CALLBACK(src, PROC_REF(make_empty_matrix)), 3 SECONDS)
/datum/data/pda/app/game/minesweeper/proc/make_empty_matrix(pay = TRUE)
minesweeper_matrix = list()
for(var/i in 1 to generation_rows)
var/list/new_row = list()
for(var/j in 1 to generation_columns)
new_row["[j]"] = list("open" = FALSE, "bomb" = FALSE, "flag" = FALSE, "around" = 0)
minesweeper_matrix["[i]"] = new_row
first_touch = TRUE
ignore_touches = FALSE
SStgui.update_uis(pda)
/datum/data/pda/app/game/minesweeper/proc/generate_matrix(x, y)
flagged_bombs = 0
setted_flags = 0
opened_cells = 0
var/list/possible_list = list()
var/num_x = text2num(x)
var/num_y = text2num(y)
var/count = 0
for(var/i in 1 to generation_rows)
for(var/j in 1 to generation_columns)
if((i in list(num_x - 1, num_x, num_x + 1)) && (j in list(num_y - 1, num_y, num_y + 1)))
continue
possible_list["[count]"] = list(i, j)
count++
for(var/bomb in 1 to generation_bombs)
var/cell = pick(possible_list)
var/coordinates = possible_list[cell]
possible_list -= cell
var/new_x = "[coordinates[1]]"
var/new_y = "[coordinates[2]]"
minesweeper_matrix[new_x][new_y]["bomb"] = TRUE
if(new_x != "1")
minesweeper_matrix["[text2num(new_x)-1]"][new_y]["around"] += 1
if(new_y != "1")
minesweeper_matrix[new_x]["[text2num(new_y)-1]"]["around"] += 1
if(new_x != "1" && new_y != "1")
minesweeper_matrix["[text2num(new_x)-1]"]["[text2num(new_y)-1]"]["around"] += 1
if(new_x != "[generation_rows]")
minesweeper_matrix["[text2num(new_x)+1]"][new_y]["around"] += 1
if(new_y != "[generation_columns]")
minesweeper_matrix[new_x]["[text2num(new_y)+1]"]["around"] += 1
if(new_x != "[generation_rows]" && new_y != "[generation_columns]")
minesweeper_matrix["[text2num(new_x)+1]"]["[text2num(new_y)+1]"]["around"] += 1
if(new_x != "1" && new_y != "[generation_columns]")
minesweeper_matrix["[text2num(new_x)-1]"]["[text2num(new_y)+1]"]["around"] += 1
if(new_x != "[generation_rows]" && new_y != "1")
minesweeper_matrix["[text2num(new_x)+1]"]["[text2num(new_y)-1]"]["around"] += 1
first_touch = FALSE
start_time = world.time
/datum/data/pda/app/game/minesweeper/proc/open_cell(x, y, start_cycle = TRUE)
. = list()
if(!minesweeper_matrix[x][y]["open"])
minesweeper_matrix[x][y]["open"] = TRUE
opened_cells += 1
if(minesweeper_matrix[x][y]["flag"])
minesweeper_matrix[x][y]["flag"] = FALSE
setted_flags -= 1
if(minesweeper_matrix[x][y]["bomb"])
flagged_bombs -= 1
if(minesweeper_matrix[x][y]["around"] == 0)
if(start_cycle)
update_zeros(x, y)
else
. = list(list(x, y))
/datum/data/pda/app/game/minesweeper/proc/update_zeros(x, y)
var/list/list_for_update = list(list(x, y))
for(var/list/coordinates in list_for_update)
var/this_x = coordinates[1]
var/this_y = coordinates[2]
var/num_x = text2num(coordinates[1])
var/num_y = text2num(coordinates[2])
var/new_x
var/new_y
if(num_x != 1)
new_x = "[num_x-1]"
list_for_update += open_cell(new_x, this_y)
if(num_y != 1)
new_y = "[num_y-1]"
list_for_update += open_cell(this_x, new_y)
if(num_x != generation_rows)
new_x = "[num_x+1]"
list_for_update += open_cell(new_x, this_y)
if(num_y != generation_columns)
new_y = "[num_y+1]"
list_for_update += open_cell(this_x, new_y)
if(num_x != 1 && num_y != 1)
new_x = "[num_x-1]"
new_y = "[num_y-1]"
if(minesweeper_matrix[new_x][this_y]["open"] && minesweeper_matrix[this_x][new_y]["open"])
list_for_update += open_cell(new_x, new_y)
if(num_x != generation_rows && num_y != generation_columns)
new_x = "[num_x+1]"
new_y = "[num_y+1]"
if(minesweeper_matrix[new_x][this_y]["open"] && minesweeper_matrix[this_x][new_y]["open"])
list_for_update += open_cell(new_x, new_y)
if(num_x != 1 && num_y != generation_columns)
new_x = "[num_x-1]"
new_y = "[num_y+1]"
if(minesweeper_matrix[new_x][this_y]["open"] && minesweeper_matrix[this_x][new_y]["open"])
list_for_update += open_cell(new_x, new_y)
if(num_x != generation_rows && num_y != 1)
new_x = "[num_x+1]"
new_y = "[num_y-1]"
if(minesweeper_matrix[new_x][this_y]["open"] && minesweeper_matrix[this_x][new_y]["open"])
list_for_update += open_cell(new_x, new_y)
#undef MINESWEEPER_ROWS
#undef MINESWEEPER_COLUMNS
#undef MINESWEEPER_BOMBS
+2
View File
@@ -2574,6 +2574,8 @@
#include "code\modules\pda\cart.dm"
#include "code\modules\pda\cart_apps.dm"
#include "code\modules\pda\core_apps.dm"
#include "code\modules\pda\games.dm"
#include "code\modules\pda\games\minesweeper.dm"
#include "code\modules\pda\messenger.dm"
#include "code\modules\pda\messenger_plugins.dm"
#include "code\modules\pda\nanobank.dm"
@@ -0,0 +1,33 @@
import { useBackend } from '../../backend';
import { Box, Button, ImageButton, Section } from '../../components';
import { Icon, IconStack } from '../../components/Icon';
export const pda_games = (props, context) => {
const { act, data } = useBackend(context);
const { games } = data;
const GetAppImage = (AppName) => {
switch (AppName) {
case 'Minesweeper':
return (
<IconStack>
<Icon ml="0" mt="10px" name="flag" size="6" color="gray" rotation={30} />
<Icon ml="9px" mt="23px" name="bomb" size="3" color="black" />
</IconStack>
);
default:
return <Icon ml="16px" mt="10px" name="gamepad" size="6" />;
}
};
return (
<Box>
{games.map((game) => (
<Button key={game.name} width="33%" textAlign="center" translucent onClick={() => act('play', { id: game.id })}>
{GetAppImage(game.name)}
<Box>{game.name}</Box>
</Button>
))}
</Box>
);
};
@@ -0,0 +1,166 @@
import { useBackend, useLocalState } from '../../backend';
import { Box, Button, Stack, Section, Table, Icon } from '../../components';
export const pda_minesweeper = (props, context) => {
const { act, data } = useBackend(context);
const [currentWindow, setWindow] = useLocalState(context, 'window', 'Game');
const AltWindow = {
'Game': 'Leaderboard',
'Leaderboard': 'Game',
};
return (
<Stack fill vertical textAlign="center">
<Stack.Item grow>{currentWindow === 'Game' ? <MineSweeperGame /> : <MineSweeperLeaderboard />}</Stack.Item>
<Stack.Item>
<Button
fluid
translucent
fontSize={2}
lineHeight={1.75}
icon={currentWindow === 'Game' ? 'book' : 'gamepad'}
onClick={() => setWindow(AltWindow[currentWindow])}
>
{AltWindow[currentWindow]}
</Button>
</Stack.Item>
</Stack>
);
};
export const MineSweeperGame = (props, context) => {
const { act, data } = useBackend(context);
const { matrix, flags, bombs } = data;
const NumColor = {
1: 'blue',
2: 'green',
3: 'red',
4: 'darkblue',
5: 'brown',
6: 'lightblue',
7: 'black',
8: 'white',
};
const handleClick = (row, cell, mode) => {
act('Square', {
'X': row,
'Y': cell,
'mode': mode,
});
};
return (
<Stack>
<Stack.Item>
{Object.keys(matrix).map((row) => (
<Box key={row}>
{Object.keys(matrix[row]).map((cell) => (
<Button
key={cell}
m={0.25}
height={2}
width={2}
className={matrix[row][cell]['open'] ? 'Minesweeper__open' : 'Minesweeper__closed'}
bold
color="transparent"
icon={
matrix[row][cell]['open']
? matrix[row][cell]['bomb']
? 'bomb'
: ''
: matrix[row][cell]['flag']
? 'flag'
: ''
}
textColor={
matrix[row][cell]['open']
? matrix[row][cell]['bomb']
? 'black'
: NumColor[matrix[row][cell]['around']]
: matrix[row][cell]['flag']
? 'red'
: 'gray'
}
onClick={(e) => handleClick(row, cell, 'bomb')}
onContextMenu={(e) => {
event.preventDefault();
handleClick(row, cell, 'flag');
}}
>
{!!matrix[row][cell]['open'] && !matrix[row][cell]['bomb'] && matrix[row][cell]['around']
? matrix[row][cell]['around']
: ' '}
</Button>
))}
</Box>
))}
</Stack.Item>
<Stack.Item grow className="Minesweeper__infobox">
<Stack vertical textAlign="left" pt={1}>
<Stack.Item pl={2} fontSize={2}>
<Icon name="bomb" color="gray" /> : {bombs}
</Stack.Item>
<Stack.Divider />
<Stack.Item pl={2} fontSize={2}>
<Icon name="flag" color="red" /> : {flags}
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
};
export const MineSweeperLeaderboard = (props, context) => {
const { act, data } = useBackend(context);
const { leaderboard } = data;
return (
<Table className="Minesweeper__list">
<Table.Row bold>
<SortButton id="name">Nick</SortButton>
<SortButton id="time">Time</SortButton>
</Table.Row>
{leaderboard &&
leaderboard
.sort((a, b) => {
const i = sortOrder ? 1 : -1;
return a[sortId].localeCompare(b[sortId]) * i;
})
.map((player, i) => (
<Table.Row key={i}>
<Table.Cell>{player.name}</Table.Cell>
<Table.Cell>{player.time}</Table.Cell>
</Table.Row>
))}
</Table>
);
};
const SortButton = (properties, context) => {
const [sortId, setSortId] = useLocalState(context, 'sortId', 'time');
const [sortOrder, setSortOrder] = useLocalState(context, 'sortOrder', false);
const { id, children } = properties;
return (
<Table.Cell>
<Button
fluid
color="transparent"
onClick={() => {
if (sortId === id) {
setSortOrder(!sortOrder);
} else {
setSortId(id);
setSortOrder(true);
}
}}
>
{children}
{sortId === id && <Icon name={sortOrder ? 'sort-up' : 'sort-down'} ml="0.25rem;" />}
</Button>
</Table.Cell>
);
};
@@ -0,0 +1,40 @@
@use '../base.scss';
@use '../functions.scss' as *;
.Minesweeper__closed {
vertical-align: middle;
background-color: lighten(base.$color-bg, 20%);
border: base.em(2px) outset lighten(base.$color-bg, 80%);
}
.Minesweeper__open {
vertical-align: middle;
text-align: center;
font-size: medium;
background-color: lighten(base.$color-bg, 50%) !important;
}
.Minesweeper__list {
tr > td {
text-align: center;
}
tr:not(:first-child) {
height: 2em;
line-height: 1.75em;
transition: background-color 50ms;
cursor: pointer;
&:hover,
&:focus {
background-color: rgba(base.$color-bg, 1);
}
}
}
.Minesweeper__infobox {
max-height: 8em;
border: base.em(3px) outset lighten(base.$color-bg, 25%);
user-select: none;
-ms-user-select: none; // Remove with Byond 516 release
}
+1
View File
@@ -63,6 +63,7 @@
@include meta.load-css('./interfaces/OreRedemption.scss');
@include meta.load-css('./interfaces/PanDEMIC.scss');
@include meta.load-css('./interfaces/PDA.scss');
@include meta.load-css('./interfaces/pda_minesweeper.scss');
@include meta.load-css('./interfaces/PdaPainter.scss');
@include meta.load-css('./interfaces/PoolController.scss');
@include meta.load-css('./interfaces/ReagentsEditor.scss');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long