diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm
index 42fecd00d3d..4f738853f1a 100644
--- a/code/modules/vending/games.dm
+++ b/code/modules/vending/games.dm
@@ -9,6 +9,7 @@
/obj/item/toy/cards/deck/cas/black = 3,
/obj/item/toy/cards/deck/kotahi = 3,
/obj/item/toy/cards/deck/tarot = 3,
+ /obj/item/toy/mahjong/wall = 2, //WS Edit -- adds mahjong deck to game vendor
/obj/item/hourglass = 2,
/obj/item/instrument/piano_synth/headphones = 4,
/obj/item/camera = 3)
diff --git a/tgstation.dme b/tgstation.dme
index d7538fa26d8..517197bdcfe 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3279,6 +3279,7 @@
#include "whitesands\code\game\objects\items\manuals.dm"
#include "whitesands\code\game\objects\items\pinpointer.dm"
#include "whitesands\code\game\objects\items\shuttle_creator.dm"
+#include "whitesands\code\game\objects\items\toys.dm"
#include "whitesands\code\game\objects\items\circuitboards\computer_circuitboards.dm"
#include "whitesands\code\game\objects\items\circuitboards\deepcore.dm"
#include "whitesands\code\game\objects\items\circuitboards\machine_circuitboards.dm"
diff --git a/whitesands/code/game/objects/items/toys.dm b/whitesands/code/game/objects/items/toys.dm
new file mode 100644
index 00000000000..7ec684c416a
--- /dev/null
+++ b/whitesands/code/game/objects/items/toys.dm
@@ -0,0 +1,299 @@
+/* Toys!
+ * Contains
+ * Mahjong Tiles
+ */
+
+/*
+|| A Deck of Mahjong tiles for playing exactly one game of chance ||
+*/
+
+//Abstract to hold generic values for mahjong-related items
+/obj/item/toy/mahjong
+ name = "abstract mahjong"
+ desc = "please do not spawn me"
+ max_integrity = 50
+ var/parentdeck = null
+ var/card_hitsound = "whitesands/sound/items/mahjongclack.ogg"
+ var/card_force = 0
+ var/card_throwforce = 5
+ var/list/card_attack_verb = list("clacked")
+
+/obj/item/toy/mahjong/proc/apply_card_vars(obj/item/toy/mahjong/newobj, obj/item/toy/mahjong/sourceobj) // Applies variables for supporting multiple types of card deck
+ if(!istype(sourceobj))
+ return
+
+/obj/item/toy/mahjong/wall
+ name = "mahjong wall"
+ desc = "A set of self-shuffling mahjong tiles."
+ icon = 'whitesands/icons/obj/toy.dmi'
+ icon_state = "mahjong_wall"
+ w_class = WEIGHT_CLASS_NORMAL
+ var/cooldown = 0
+ var/obj/machinery/computer/holodeck/holo = null // Holodeck mahjong should not be infinite
+ var/list/tiles = list()
+
+/obj/item/toy/mahjong/wall/Initialize()
+ . = ..()
+ populate_wall()
+
+///Generates all the tiles within the wall.
+/obj/item/toy/mahjong/wall/proc/populate_wall()
+ for(var/suit in list("pin", "man", "sou"))
+ tiles += "red 5-[suit]"
+ for(var/i in 1 to 9)
+ for(var/c = 0;c<3;c++)
+ tiles += "[i]-[suit]"
+ if(i != 5)
+ tiles += "[i]-[suit]"
+ for(var/honor in list("ton","nan","sha","pei","haku","hatsu","chun"))
+ for(var/c = 0;c<4;c++)
+ tiles += honor
+
+//ATTACK HAND IGNORING PARENT RETURN VALUE
+/obj/item/toy/mahjong/wall/attack_hand(mob/living/user)
+ draw_card(user)
+
+/obj/item/toy/mahjong/wall/proc/draw_card(mob/living/user)
+ var/mob/living/L = user
+ if(!(L.mobility_flags & MOBILITY_PICKUP))
+ return
+ var/choice = null
+ if(tiles.len == 0)
+ to_chat(user, "There are no more tiles to draw!")
+ return
+ var/obj/item/toy/mahjong/singletile/H = new/obj/item/toy/mahjong/singletile(user.loc)
+ if(holo)
+ holo.spawned += H // track them leaving the holodeck
+ choice = tiles[1]
+ H.cardname = choice
+ H.parentdeck = src
+ var/O = src
+ H.apply_card_vars(H,O)
+ src.tiles.Cut(1,2)
+ H.pickup(user)
+ user.put_in_hands(H)
+ user.visible_message("[user] draws a tile from the wall.", "You draw a tile from the wall.")
+ update_icon()
+ return H
+
+/obj/item/toy/mahjong/wall/attack_self(mob/user)
+ if(cooldown < world.time - 50)
+ tiles = shuffle(tiles)
+ playsound(src, 'whitesands/sound/items/mahjongshuffle.ogg', 50, TRUE)
+ user.visible_message("[user] shuffles the wall.", "You shuffle the wall.")
+ cooldown = world.time
+
+/obj/item/toy/mahjong/wall/attackby(obj/item/I, mob/living/user, params)
+ if(istype(I, /obj/item/toy/mahjong/singletile))
+ var/obj/item/toy/mahjong/singletile/SC = I
+ if(SC.parentdeck == src)
+ if(!user.temporarilyRemoveItemFromInventory(SC))
+ to_chat(user, "The tile is stuck to your hand, you can't add them to the deck!")
+ return
+ tiles += SC.cardname
+ user.visible_message("[user] adds a tile to the end of the wall.","You add the tile to the end of the wall.")
+ qdel(SC)
+ else
+ to_chat(user, "You can't mix tiles from other walls!")
+ update_icon()
+ else if(istype(I, /obj/item/toy/mahjong/tilegroup))
+ var/obj/item/toy/mahjong/tilegroup/CH = I
+ if(CH.parentdeck == src)
+ if(!user.temporarilyRemoveItemFromInventory(CH))
+ to_chat(user, "The tiles are stuck to your hand, you can't add it to the deck!")
+ return
+ tiles += CH.currentgroup
+ user.visible_message("[user] puts [user.p_their()] group of tiles in the wall.", "You put the group of tiles in the wall.")
+ qdel(CH)
+ else
+ to_chat(user, "You can't mix tiles from other walls!")
+ update_icon()
+ else
+ return ..()
+
+/obj/item/toy/mahjong/wall/MouseDrop(atom/over_object)
+ . = ..()
+ var/mob/living/M = usr
+ if(!istype(M) || !(M.mobility_flags & MOBILITY_PICKUP))
+ return
+ if(Adjacent(usr))
+ if(over_object == M && loc != M)
+ M.put_in_hands(src)
+ to_chat(usr, "You pick up the wall.")
+
+ else if(istype(over_object, /obj/screen/inventory/hand))
+ var/obj/screen/inventory/hand/H = over_object
+ if(M.putItemFromInventoryInHandIfPossible(src, H.held_index))
+ to_chat(usr, "You pick up the wall.")
+
+ else
+ to_chat(usr, "You can't reach it from here!")
+
+
+
+/obj/item/toy/mahjong/tilegroup
+ name = "tile group"
+ desc = "A number of mahjong tiles."
+ icon = 'whitesands/icons/obj/toy.dmi'
+ icon_state = "none"
+ w_class = WEIGHT_CLASS_TINY
+ var/list/currentgroup = list()
+ var/choice = null
+
+/obj/item/toy/mahjong/tilegroup/attack_self(mob/user)
+ user.set_machine(src)
+ interact(user)
+
+
+/obj/item/toy/mahjong/tilegroup/ui_interact(mob/user)
+ . = ..()
+ var/dat = "You have:
"
+ for(var/t in currentgroup)
+ dat += "A [t].
"
+ dat += "Which tile will you remove?"
+ var/datum/browser/popup = new(user, "tilegroup", "Group of tiles", 400, 240)
+ popup.set_content(dat)
+ popup.open()
+
+/obj/item/toy/mahjong/tilegroup/Topic(href, href_list)
+ if(..())
+ return
+ if(usr.stat || !ishuman(usr))
+ return
+ var/mob/living/carbon/human/cardUser = usr
+ if(!(cardUser.mobility_flags & MOBILITY_USE))
+ return
+ var/O = src
+ if(href_list["pick"])
+ if (cardUser.is_holding(src))
+ var/choice = href_list["pick"]
+ var/obj/item/toy/mahjong/singletile/C = new/obj/item/toy/mahjong/singletile(cardUser.loc)
+ src.currentgroup -= choice
+ C.parentdeck = src.parentdeck
+ C.cardname = choice
+ C.apply_card_vars(C,O)
+ C.pickup(cardUser)
+ cardUser.put_in_hands(C)
+ cardUser.visible_message("[cardUser] draws a tile from [cardUser.p_their()] group.", "You take the [C.cardname] from your group.")
+
+ interact(cardUser)
+ update_sprite()
+ if(src.currentgroup.len == 1)
+ var/obj/item/toy/mahjong/singletile/N = new/obj/item/toy/mahjong/singletile(src.loc)
+ N.parentdeck = src.parentdeck
+ N.cardname = src.currentgroup[1]
+ N.apply_card_vars(N,O)
+ qdel(src)
+ N.pickup(cardUser)
+ cardUser.put_in_hands(N)
+ to_chat(cardUser, "You also take [currentgroup[1]] and hold it.")
+ cardUser << browse(null, "window=tilegroup")
+ return
+
+/obj/item/toy/mahjong/tilegroup/attackby(obj/item/toy/mahjong/singletile/C, mob/living/user, params)
+ if(istype(C))
+ if(C.parentdeck == src.parentdeck)
+ src.currentgroup += C.cardname
+ playsound(src, src.card_hitsound, 50, TRUE)
+ user.visible_message("[user] adds a tile to [user.p_their()] group.", "You add the [C.cardname] to your group.")
+ qdel(C)
+ interact(user)
+ update_sprite(src)
+ else
+ to_chat(user, "You can't mix tiles from other walls!")
+ else
+ return ..()
+
+/obj/item/toy/mahjong/tilegroup/apply_card_vars(obj/item/toy/mahjong/newobj,obj/item/toy/mahjong/sourceobj)
+ ..()
+ update_sprite()
+ newobj.card_hitsound = sourceobj.card_hitsound
+ newobj.card_force = sourceobj.card_force
+ newobj.card_throwforce = sourceobj.card_throwforce
+ newobj.card_attack_verb = sourceobj.card_attack_verb
+ newobj.resistance_flags = sourceobj.resistance_flags
+
+/**
+ * This proc updates the sprite for when you create a hand of tiles
+ */
+/obj/item/toy/mahjong/tilegroup/proc/update_sprite()
+ cut_overlays()
+ var/overlay_mahjong = currentgroup.len
+ //establish k, which is the remainder of i and 6, pix_x, which is k * 5 and one to the right, and pix_y, which is 7 down for every 6 in i
+ //then add an overlay for the tile at pix_x and pix_y
+ for(var/i = 1; i <= overlay_mahjong; i++)
+ var/x = i
+ var/k = (i-1)%6
+ var/pix_x = 1+(k*5)
+ var/pix_y = 0
+ while(x > 6)
+ x -= 6
+ pix_y -= 7
+ var/tile_overlay = image(icon=src.icon,icon_state="mini [currentgroup[i]]",pixel_x=pix_x,pixel_y=pix_y)
+ add_overlay(tile_overlay)
+
+/obj/item/toy/mahjong/singletile
+ name = "mahjong tile"
+ desc = "A tile used to play mahjong. Made of hard plastic."
+ icon = 'whitesands/icons/obj/toy.dmi'
+ icon_state = "haku"
+ w_class = WEIGHT_CLASS_TINY
+ var/cardname = "haku"
+ pixel_x = -5
+
+/obj/item/toy/mahjong/singletile/Initialize()
+ . = ..()
+ icon_state = "[cardname]"
+
+/obj/item/toy/mahjong/singletile/examine(mob/user)
+ . = ..()
+ if(ishuman(user))
+ var/mob/living/carbon/human/cardUser = user
+ if(cardUser.is_holding(src))
+ cardUser.visible_message("[cardUser] checks [cardUser.p_their()] card.", "The card reads: [cardname].")
+ else
+ . += "You need to have the card in your hand to check it!"
+
+/obj/item/toy/mahjong/singletile/attackby(obj/item/I, mob/living/user, params)
+ if(istype(I, /obj/item/toy/mahjong/singletile/))
+ var/obj/item/toy/mahjong/singletile/C = I
+ if(C.parentdeck == src.parentdeck)
+ playsound(src, src.card_hitsound, 50, TRUE)
+ var/obj/item/toy/mahjong/tilegroup/H = new/obj/item/toy/mahjong/tilegroup(user.loc)
+ H.currentgroup += C.cardname
+ H.currentgroup += src.cardname
+ H.parentdeck = C.parentdeck
+ H.apply_card_vars(H,C)
+ to_chat(user, "You combine the [C.cardname] and the [src.cardname] into a hand.")
+ qdel(C)
+ qdel(src)
+ H.pickup(user)
+ user.put_in_active_hand(H)
+ else
+ to_chat(user, "You can't mix tiles from other walls!")
+
+ if(istype(I, /obj/item/toy/mahjong/tilegroup/))
+ var/obj/item/toy/mahjong/tilegroup/H = I
+ if(H.parentdeck == parentdeck)
+ playsound(src, src.card_hitsound, 50, TRUE)
+ H.currentgroup += cardname
+ user.visible_message("[user] adds a tile to [user.p_their()] group.", "You add the [cardname] to your group.")
+ qdel(src)
+ H.interact(user)
+ H.update_sprite()
+ else
+ to_chat(user, "You can't mix tiles from other walls!")
+ else
+ return ..()
+
+/obj/item/toy/mahjong/singletile/apply_card_vars(obj/item/toy/mahjong/singletile/newobj,obj/item/toy/mahjong/sourceobj)
+ ..()
+ newobj.icon_state = newobj.cardname
+ newobj.card_hitsound = sourceobj.card_hitsound
+ newobj.hitsound = newobj.card_hitsound
+ newobj.card_force = sourceobj.card_force
+ newobj.force = newobj.card_force
+ newobj.card_throwforce = sourceobj.card_throwforce
+ newobj.throwforce = newobj.card_throwforce
+ newobj.card_attack_verb = sourceobj.card_attack_verb
+ newobj.attack_verb = newobj.card_attack_verb
diff --git a/whitesands/icons/obj/toy.dmi b/whitesands/icons/obj/toy.dmi
new file mode 100644
index 00000000000..d31c047a1f5
Binary files /dev/null and b/whitesands/icons/obj/toy.dmi differ
diff --git a/whitesands/sound/items/mahjongclack.ogg b/whitesands/sound/items/mahjongclack.ogg
new file mode 100644
index 00000000000..9e146d9494c
Binary files /dev/null and b/whitesands/sound/items/mahjongclack.ogg differ
diff --git a/whitesands/sound/items/mahjongshuffle.ogg b/whitesands/sound/items/mahjongshuffle.ogg
new file mode 100644
index 00000000000..1f4033a09cf
Binary files /dev/null and b/whitesands/sound/items/mahjongshuffle.ogg differ