Skip to content

Instantly share code, notes, and snippets.

@Waffle1434
Created October 16, 2025 06:45
Show Gist options
  • Select an option

  • Save Waffle1434/4d7b8d08a827d5bcc1b2843a38d325bb to your computer and use it in GitHub Desktop.

Select an option

Save Waffle1434/4d7b8d08a827d5bcc1b2843a38d325bb to your computer and use it in GitHub Desktop.
const vec3 = (x = 0, y = 0, z = 0) => mod.CreateVector(x, y, z);
class vec2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
to_key(): string { return `${this.x},${this.y}`; }
}
enum InputDir { Left, Right, Up, Down }
class Cell {
box: mod.UIWidget;
constructor(game: SnakeGame, pos: vec2, color: mod.Vector) {
this.box = game.create_box(pos, color);
}
}
class Apple extends Cell {
goodness: number;
constructor(game: SnakeGame, pos: vec2, color: mod.Vector, goodness: number) {
super(game, pos, color);
this.goodness = goodness;
game.cells.set(pos.to_key(), this);
}
}
class SnakeCell extends Cell {
next : SnakeCell | null = null;
key : string;
constructor(game: SnakeGame, pos: vec2, color: mod.Vector) {
super(game, pos, color);
this.key = pos.to_key();
game.cells.set(this.key, this);
}
move(game: SnakeGame, pos: vec2) {
game.cells.delete(this.key);
this.key = pos.to_key();
game.cells.set(this.key, this);
mod.SetUIWidgetPosition(this.box, game.cell_to_px(pos));
}
}
class Snake {
cells : number = 0;
cells_to_add : number;
head_pos : vec2;
color : mod.Vector;
head : SnakeCell | null = null;
tail : SnakeCell | null = null;
constructor(game: SnakeGame, pos: vec2, cells: number, color: mod.Vector) {
this.cells = cells;
this.cells_to_add = cells;
this.head_pos = pos;
this.color = color;
this.tail = this.add_head(game);
}
set_head(cell: SnakeCell) {
if (this.head != null)
this.head.next = cell;
this.head = cell;
}
add_head(game: SnakeGame): SnakeCell {
const cell = new SnakeCell(game, this.head_pos, this.color);
this.set_head(cell);
this.cells_to_add--;
return cell;
}
update(game: SnakeGame, input: vec2) {
this.head_pos.x += input.x;
this.head_pos.y += input.y;
const head_pos_key = this.head_pos.to_key();
const eat_cell = game.cells.get(head_pos_key);
if (eat_cell instanceof SnakeCell ||
this.head_pos.x < 0 || this.head_pos.x >= game.size.x ||
this.head_pos.y < 0 || this.head_pos.y >= game.size.y
) {
game.end();
return;
}
if (eat_cell instanceof Apple) {
mod.DeleteUIWidget(eat_cell.box); // TODO: move instead?
game.cells.delete(head_pos_key);
this.cells_to_add += eat_cell.goodness;
this.cells += eat_cell.goodness;
game.update_score();
game.add_apple();
}
if (this.cells_to_add > 0) {
this.add_head(game);
}
else if (this.tail != null) {
const head_cell = this.tail;
head_cell.move(game, this.head_pos);
if (this.head != this.tail)
this.set_head(head_cell);
if (head_cell.next != null) {
this.tail = head_cell.next;
head_cell.next = null;
}
}
}
}
class SnakeGame {
size : vec2;
cell_size : number;
cells : Map<string, Cell>;
background : mod.UIWidget;
player : mod.Player;
do_score : boolean;
paused : boolean = false;
over : boolean = false;
kill : boolean = true;
cell_size_vec : mod.Vector;
box_id : number = 0;
snake_color : mod.Vector = vec3(1, 1, 1);
apple_color : mod.Vector = vec3(1, 0, 0);
snake : Snake;
constructor(size: vec2, cell_size: number, root: mod.UIWidget, player: mod.Player, do_score: boolean) {
this.size = size;
this.cell_size = cell_size;
this.cells = new Map<string, Cell>();
this.player = player;
this.do_score = do_score;
this.cell_size_vec = vec3(cell_size, cell_size, 0);
const color_background = vec3(0, 0, 0);
const background_name = "snake_background";
const background_size = vec3(size.x * cell_size, size.y * cell_size, 0);
mod.AddUIContainer(background_name, vec3(0, 0, 0), background_size, mod.UIAnchor.Center, root, true, 0, color_background, 1, mod.UIBgFill.Solid, player);
this.background = mod.FindUIWidgetWithName(background_name);
this.snake = new Snake(this, new vec2(0, 0), 3, this.snake_color);
if (this.do_score) {
mod.SetScoreboardType(mod.ScoreboardType.CustomFFA);
mod.SetScoreboardColumnNames(mod.Message("Snake Length"));
this.update_score();
}
}
cell_to_px(pos: vec2): mod.Vector {
return vec3(pos.x * this.cell_size, pos.y * this.cell_size, 0);
}
create_box(pos: vec2, color: mod.Vector): mod.UIWidget {
const name = `snake_${this.box_id++}`;
mod.AddUIContainer(name, this.cell_to_px(pos), this.cell_size_vec, mod.UIAnchor.BottomLeft, this.background, true, 0, color, 1, mod.UIBgFill.Solid, this.player);
return mod.FindUIWidgetWithName(name);
}
get_random_empty_cell(): vec2 {
// TODO: check if grid is full!
while (true) {
const rand_x = mod.RoundToInteger(mod.RandomReal(0, this.size.x - 1));
const rand_y = mod.RoundToInteger(mod.RandomReal(0, this.size.y - 1));
const rand_pos = new vec2(rand_x, rand_y);
if (!this.cells.has(rand_pos.to_key()))
return rand_pos;
}
}
add_apple(): Apple {
return new Apple(this, this.get_random_empty_cell(), this.apple_color, 5);
}
update_score() {
if (this.do_score)
mod.SetScoreboardPlayerValues(this.player, this.snake.cells);
}
update(input: InputDir) {
const input_vec = new vec2(
input == InputDir.Left ? -1 : input == InputDir.Right ? 1 : 0,
input == InputDir.Up ? 1 : input == InputDir.Down ? -1 : 0
);
this.snake.update(this, input_vec);
}
end() {
this.over = true;
mod.AddUIText("snake_game_over", vec3(0, 0, 0), vec3(160, 32, 0), mod.UIAnchor.Center, this.background,
true, 0, vec3(0, 0, 0), 1, mod.UIBgFill.Solid, mod.Message("GAME OVER"), 32, vec3(1, 1, 1), 1, mod.UIAnchor.Center);
}
}
class Controller {
player : mod.Player;
input_dir : InputDir | null = null;
input_dir_l : InputDir | null = null;
queued_input : InputDir | null = null;
button_l : mod.UIWidget | null = null;
button_l_name : string | null = null;
disabled_buttons : mod.UIWidget[] = [];
static inverse_input = {
[InputDir.Left]: InputDir.Right,
[InputDir.Right]: InputDir.Left,
[InputDir.Up]: InputDir.Down,
[InputDir.Down]: InputDir.Up
};
constructor(player: mod.Player) {
this.player = player;
}
async create_buttons() {
const button_center = this.create_button("snake_button_center", 0, 0, game.background);
controller.set_button_l(button_center, "snake_button_center");
await mod.Wait(0.5);
this.create_button("snake_button_l", -1, 0, game.background);
this.create_button("snake_button_r", 1, 0, game.background);
this.create_button("snake_button_u", 0, 1, game.background);
this.create_button("snake_button_d", 0, -1, game.background);
this.create_button("snake_button_ul", -1, 1, game.background);
this.create_button("snake_button_ur", 1, 1, game.background);
this.create_button("snake_button_dl", -1, -1, game.background);
this.create_button("snake_button_dr", 1, -1, game.background);
}
create_button(name: string, x: number, y: number, background: mod.UIWidget): mod.UIWidget {
const button_size = 16;
const x_base = -64;
const y_base = -64;
mod.AddUIButton(name,
vec3(x_base + x * button_size, y_base + y * button_size, 0),
vec3(button_size, button_size, 0),
mod.UIAnchor.BottomLeft, background, true, 0,
vec3(1.0, 1.0, 1.0), 1.0, mod.UIBgFill.Solid, true,
vec3(0.5, 0.5, 0.5), 1.0,
vec3(1.0, 1.0, 1.0), 0.5,
vec3(1.0, 1.0, 1.0), 1.0,
vec3(0.0, 0.0, 1.0), 1.0,
vec3(1.0, 0.0, 0.0), 1.0,
this.player
);
const button = mod.FindUIWidgetWithName(name);
mod.EnableUIButtonEvent(button, mod.UIButtonEvent.FocusIn, true);
mod.EnableUIButtonEvent(button, mod.UIButtonEvent.ButtonDown, true);
return button;
}
set_input_state(enabled: boolean) {
mod.EnableInputRestriction(this.player, mod.RestrictedInputs.MoveForwardBack, enabled);
mod.EnableInputRestriction(this.player, mod.RestrictedInputs.MoveLeftRight, enabled);
//mod.EnableInputRestriction(this.player, mod.RestrictedInputs.Jump, enabled);
}
set_button_l(button: mod.UIWidget, name: string) {
this.button_l = button;
this.button_l_name = name;
}
set_input(input: InputDir) {
this.input_dir = input;
for (const button of this.disabled_buttons) {
mod.SetUIWidgetVisible(button, true);
}
this.disabled_buttons.length = 0;
for (const name of this.get_disabled_inputs(input)) {
const button = mod.FindUIWidgetWithName(name);
mod.SetUIWidgetVisible(button, false);
this.disabled_buttons.push(button);
}
}
get_disabled_inputs(input: InputDir): string[] {
switch (input) {
case InputDir.Left:
case InputDir.Right:
return ["snake_button_l", "snake_button_r"];
case InputDir.Up:
case InputDir.Down:
return ["snake_button_u", "snake_button_d"];
}
}
button_to_direction(button_name: string): InputDir | null {
switch (button_name) {
case "snake_button_l": return InputDir.Left;
case "snake_button_r": return InputDir.Right;
case "snake_button_u": return InputDir.Up;
case "snake_button_d": return InputDir.Down;
default:
if (this.input_dir_l == null || this.button_l_name == null)
return null;
const next_input = ({
snake_button_l: { snake_button_ul: InputDir.Up, snake_button_dl: InputDir.Down },
snake_button_r: { snake_button_ur: InputDir.Up, snake_button_dr: InputDir.Down },
snake_button_u: { snake_button_ul: InputDir.Left, snake_button_ur: InputDir.Right },
snake_button_d: { snake_button_dl: InputDir.Left, snake_button_dr: InputDir.Right },
} as any)[this.button_l_name]?.[button_name] ?? null;
if (next_input == Controller.inverse_input[this.input_dir_l]) {
this.queued_input = next_input;
return null;
}
return next_input;
}
}
async snake_OnPlayerUIButtonEvent(
player: mod.Player,
button: mod.UIWidget,
button_event: mod.UIButtonEvent
) {
const button_name = mod.GetUIWidgetName(button);
if (button_name.startsWith("snake_") && this.button_l != null) {
if (mod.Equals(button_event, mod.UIButtonEvent.FocusIn)) {
const new_input = this.button_to_direction(button_name);
mod.SetUIWidgetName(this.button_l, button_name);
mod.SetUIWidgetName(button, "snake_button_center");
const pos_old = mod.GetUIWidgetPosition(this.button_l);
const pos_new = mod.GetUIWidgetPosition(button);
mod.SetUIWidgetPosition(button, pos_old);
mod.SetUIWidgetPosition(this.button_l, pos_new);
this.set_button_l(button, button_name);
if (new_input != null)
this.set_input(new_input);
}
else if (mod.Equals(button_event, mod.UIButtonEvent.ButtonDown))
game.paused = !game.paused;
}
}
}
let game : SnakeGame;
let controller : Controller;
function is_snake_gadget_out(player: mod.Player): boolean {
return mod.IsInventorySlotActive(player, mod.InventorySlots.GadgetOne);
}
async function game_session(player: mod.Player) {
const root = mod.GetUIRoot();
mod.AddUIContainer("snake_blur_background", vec3(0, 0, 0), vec3(8192, 8192, 0), mod.UIAnchor.Center, root, true, 0, vec3(1, 1, 1), 1, mod.UIBgFill.Blur, player);
game = new SnakeGame(new vec2(16, 16), 32, root, player, true);
game.add_apple();
controller = new Controller(player);
await controller.create_buttons();
controller.set_input_state(true);
controller.set_input(InputDir.Right);
while (mod.GetSoldierState(player, mod.SoldierStateBool.IsAlive) && !game.over) {
if (!is_snake_gadget_out(player)) {
//paused = true;
break;
}
if (!game.paused && controller.input_dir != null) {
game.update(controller.input_dir);
controller.input_dir_l = controller.input_dir;
if (controller.queued_input != null) {
controller.set_input(controller.queued_input);
controller.queued_input = null;
}
if (game.over) {
await mod.Wait(2);
if (game.kill)
mod.Kill(player);
}
}
await mod.Wait(0.25);
}
mod.DeleteAllUIWidgets();
controller.set_input_state(false);
}
async function snake_OnPlayerDeployed(player: mod.Player) {
mod.AddEquipment(player, mod.Gadgets.Deployable_EOD_Bot);
//mod.ForceSwitchInventory(player, mod.InventorySlots.GadgetOne);
mod.DisplayHighlightedWorldLogMessage(mod.Message("SELECT GADGET TO PLAY SNAKE! (WASD)"));
await mod.Wait(2);
mod.DisplayHighlightedWorldLogMessage(mod.Message("Press G in scoreboard to like!"));
while (mod.GetSoldierState(player, mod.SoldierStateBool.IsAlive)) {
if (is_snake_gadget_out(player)) {
await mod.Wait(0.5);
await game_session(player);
}
await mod.Wait(0.25);
}
}
export async function OnPlayerDeployed(player: mod.Player) {
await snake_OnPlayerDeployed(player);
}
export async function OnPlayerUIButtonEvent(
player: mod.Player,
button: mod.UIWidget,
button_event: mod.UIButtonEvent
) {
await controller.snake_OnPlayerUIButtonEvent(player, button, button_event);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment