Gunn Elimination Blog #2

Elimination updates since the last post. The game starts in 2 days!

Gunn Elimination Blog #2

Gunn Elimination starts in 2 days and we have about 650 signups (250 more than last year)! Lately, I've been doing more logistical stuff, like handing out plushies and organizing the rules, but I've also added some new features.

But let's look at the site first.

Final UI Design

Not too different from what I had previously, but I think it looks a little better. Since the game didn't start, a lot of the fields are left blank, so maybe I'll update it later.

/app
/app/leaderboard
/app/rules
/app/profile/95030486

Supabase Pro

I didn't want to take any risks since 650 people is a lot to deal with, so I paid for Supabase Pro, which comes with daily backups. Also, it's lowkey a scam since I had to pay an extra $10 to make it look like this.

No more "yihigqyfdifpodmnguxr.supabase.co"

But also like it broke for some other people so I may end up not going with the custom domain (so I basically wasted $35!).

Game Messages

I made a new table to make messages since it's important to keep everyone updated. Pretty simple.

Calendar and Kill Cutoff

We mostly used last year's rules, but we made the game longer and added some fun rules, like wearing JORTS.

This year, to avoid random people who don't play the game but just hide all week, we decided to implement a kill cutoff. Basically, if you don't have more than 1 kill by the end of the week, you're automatically eliminated. I might do a last-minute removal of this rule if not enough people will be left, but I think it's a cool rule this year.

Elimination Calendar 2024

Kill Cutoff Code

Not gonna lie, the code is terrible because I was lazy and used a little ChatGPT. It basically loads all players into a JS array, then filters out people with enough kills to stay alive. Then, we go one by one and delete the remaining people. It literally takes so long to load (like 20 seconds), so I'm worried that something might crash during the game. But hopefully not!

import { error, json } from '@sveltejs/kit';

export const POST = async ({ url, locals: { supabaseAdmin, getSession, getRole }, request }) => {
	const role = await getRole();
	if (role !== 'Admin') {
		throw error(403, { message: 'Unauthorized' });
	}

	const { kill_requirement } = await request.json();

	if (!kill_requirement) throw error(422, 'Invalid kill_requirement');

	// Fetch all players
	const { data: playersData, error: playersError } = await supabaseAdmin
		.from('players')
		.select('*');

	if (!playersData || playersError) throw error(500, 'Error with players table');

	// Filter players with less than one kill
	const playersToUpdate = playersData.filter((player) => player.kill_arr.length < kill_requirement);

	// Update these players
	for (const player of playersToUpdate) {
		const { error: updateError } = await supabaseAdmin
			.from('players')
			.update({ alive: false })
			.eq('id', player.id); // Assuming each player has a unique 'id' field

		if (updateError) {
			throw updateError;
		}
	}

	return json({ message: 'Successfully cutoff players' });
};

/api/game/admin/playercutoff

And while I was testing it out, I had a mini heart attack. I thought I deleted my entire players table (with 500 people at the time). Turns out, I just had a filter when viewing the table.

Mini heart attack

More Code Updates

I have a lot of small improvements that I added as well.

Retain Kill Code If Not Logged In

When scanning a QR code, it will open a browser and attempt the kill. But it won't work if you're not logged in. This was bad because then you'd have to log in and then scan the code again.

Basically, if we're not logged in, then we'll redirect to /login?killcode=[kill_code_here]. On the login page, we'll check if there's a code and save it to localstorage.

onMount(() => {
    if (killcode) {
        localStorage.setItem('loginkillcode', killcode);
    }

    loaded = true;
});

Then when we go to the app homepage, we'll check for a killcode in localStorage and try it.

onMount(async () => {
    const loginkillcode = localStorage.getItem('loginkillcode');
    if (loginkillcode) {
        inputKillCode = loginkillcode;
        localStorage.removeItem('loginkillcode');
        await submitCode();
    }
});

Fixing BIG Security Vulnerability

Try finding the issue with this.

import { json, error } from '@sveltejs/kit';

export const POST = async ({ url, locals: { supabaseAdmin, getSession, getRole }, request }) => {

	const { student_id } = await request.json();

	// killedPlayer = the player we're force killing
	// originalPlayer = the player who had this guy as the target

	const { data: killedPlayer } = await supabaseAdmin
		.from('players')
		.select('id')
		.eq('student_id', student_id)
		.single();

	if (!killedPlayer) throw error(500, 'Error fetching player with that student id');

	// Delete the player's entry in the targets table
	const { data: killedPlayerTarget } = await supabaseAdmin
		.from('targets')
		.select('target')
		.eq('id', killedPlayer.id)
		.single();
	if (!killedPlayerTarget) throw error(500, 'Error deleting target');

	// Delete the player's entry in the targets table
	const { error: deleteError } = await supabaseAdmin
		.from('targets')
		.delete()
		.eq('id', killedPlayer.id);
	if (deleteError) throw error(500, 'Error deleting target');

	// Player's target is not alive anymore
	const { error: updateTargetAliveError } = await supabaseAdmin
		.from('players')
		.update({ alive: false })
		.eq('id', killedPlayer.id);
	if (updateTargetAliveError) throw error(500, 'Error updating target alive status');

	// Get the player who had this player
	const { data: originalPlayerTargetData, error: getPlayerError } = await supabaseAdmin
		.from('targets')
		.select('id')
		.eq('target', killedPlayer.id)
		.single();
	if (!originalPlayerTargetData) throw error(500, 'Error fetching the original player target data');

	const { data: originalPlayer } = await supabaseAdmin
		.from('players')
		.select('*')
		.eq('id', originalPlayerTargetData.id)
		.single();
	if (!originalPlayer) throw error(500, 'Error fetching original player');

	const { error: updateError } = await supabaseAdmin
		.from('players')
		.update({ kill_arr: [...originalPlayer.kill_arr, killedPlayer.id] })
		.eq('id', originalPlayer.id);
	if (updateError) throw error(500, 'Error updating player data');

	// Record kill in kill_feed table
	const { error: killFeedError } = await supabaseAdmin
		.from('kill_feed')
		.insert({ player_id: originalPlayer.id, target_id: killedPlayer.id });
	if (killFeedError) throw error(500, 'Error inserting into kill feed');

	// Update player's target to the target's target
	const { error: setNewTargetError } = await supabaseAdmin
		.from('targets')
		.update({ target: killedPlayerTarget.target })
		.eq('id', originalPlayer.id);
	if (setNewTargetError) throw error(500, 'Error setting new target');

	// Redirect to app or return success
	return json({ mesage: 'Successfully force killed player' });
};

/api/game/admin/forcekillplayer

If you're me, then you probably found it. But if you're not, then you have no idea what I'm talking about lol.

If I didn't include this below, then anyone can force-kill players, so long as they know how to do a POST request.

const role = await getRole();
if (role !== 'Admin') {
    throw error(403, { message: 'Unauthorized' });
}

Checking for permission

Obviously that's really bad, so good thing I found it!

Backup Database as CSV

I wrote this before I decided to pay for Supabase Pro, but I think it was a cool thing to learn regardless.

Basically, I had lot's of help from Google and ChatGPT to figure out how to download my table as a CSV file.

import { error } from '@sveltejs/kit';
import JSZip from 'jszip';
import { format } from 'date-fns'; // TODO: make this work with dayjs cuz I don't need two date packages

export const GET = async ({ locals: { supabaseAdmin, getRole } }) => {
	const role = await getRole();
	if (role !== 'Admin') {
		throw error(403, { message: 'Unauthorized' });
	}

	const tables = ['players', 'kill_feed', 'targets'];
	const zip = new JSZip();
	const folderName = format(new Date(), 'yyyy-MM-dd');
	const folder = zip.folder(folderName);

	if (!folder) throw error(500, 'Error creating folder');

	for (const table of tables) {
		const { data, error: tableError } = await supabaseAdmin.from(table).select('*').csv();
		if (tableError) throw error(500, `Error fetching ${table}`);
		folder.file(`${table}.csv`, data);
	}

	const zipBlob = await zip.generateAsync({ type: 'blob' });

	return new Response(zipBlob, {
		status: 200,
		headers: {
			'Content-Disposition': `attachment; filename="${folderName}.zip"`,
			'Content-Type': 'application/zip'
		}
	});
};

/api/game/admin/backuptable

We can then do a get request and download the blobs, or whatever that means.

const exportTables = async () => {
    // Download zip (based on ChatGPT)
    try {
        const response = await fetch($page.url.origin + '/api/game/admin/backuptable');
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }

        const blob = await response.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'tables.zip';
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a);
    } catch (error) {
        console.error('Error:', error);
    }
};

Function to Download Zip Files

Get Today's Challenge

Do display the current challenge, I just hard coded a function to return the right string given the date. I used ChatGPT to turn my table into a switch statement, cuz that's a lot of manual labor.

// Lol thanks ChatGPT
export function getTodaysChallenge() {
	const currentDate = new Date();
	const day = currentDate.getDate();
	let rule = '';

	switch (day) {
		case 28:
			rule = 'Targets Assigned 10 PM (Sign ups close)';
			break;
		case 29:
			rule = 'GAME STARTS. 12 AM: To stay safe, players must hold the animal with their right hand';
			break;
		case 30:
			rule = 'To stay safe, players must be holding the animal with both hands';
			break;
		case 31:
			rule = 'Targets Change at 10 PM: To stay safe, players must be holding animal BELOW waist';
			break;
		case 1:
			rule = 'To stay safe, players must be holding animal ABOVE their shoulder';
			break;
		case 2:
			rule = 'To stay safe, players must be under a roof or overhang';
			break;
		case 3:
			rule =
				'Targets Change at 10 PM: Players with less than one kill are automatically eliminated';
			break;
		case 4:
		case 11:
		case 16:
		case 19:
			rule = 'On weekends, players must abide by all rules and must carry a plushie to be safe';
			break;
		case 5:
			rule = 'To stay safe, players must be touching a wall';
			break;
		case 6:
			rule = 'To stay safe, players must be wearing a HAT.';
			break;
		case 7:
			rule = 'Targets Change at 10 PM: To stay safe, players must be wearing JORTS (jean shorts).';
			break;
		case 8:
			rule = 'To stay safe, players must be wearing a wig';
			break;
		case 9:
			rule = 'To stay safe, players must be SITTING OR LAYING DOWN ON THE GROUND.';
			break;
		case 10:
			rule =
				'Targets Change at 10 PM: Players with less than two kills are automatically eliminated';
			break;
		case 12:
			rule = 'To eliminate others, players must have at least one shoe off.';
			break;
		case 13:
			rule = 'To eliminate others, players must be carrying a football in one hand.';
			break;
		case 14:
			rule =
				'Targets Change at 10 PM:. To eliminate others, players must give their target a flower immediately after eliminating them.';
			break;
		case 15:
			rule = 'To eliminate others, players must be wearing a tie around their neck.';
			break;
		case 20:
			rule = 'NO ONE IS SAFE. (can be tagged anytime even w/animal)';
			break;
		default:
			rule = 'On weekends, players must abide by all rules and must carry a plushie to be safe';
			break;
	}
	return rule;
}

Conclusion

That's it for now. I might do another post if something big happens. My friend asked to help improve the UI like yesterday, but I told him it was too late and I didn't want to give him the secret keys since something could accidentally go wrong. I'm hoping the game goes well, and that I can at least make top 10.

Also, we are considering doing an unofficial senior assassin, with water guns and no safety zones. That will be way more fun and crazy in my opinion.