flappy bird first version

This commit is contained in:
lily 2024-04-03 10:45:32 +02:00
parent 0395b4ffe7
commit a899f0b20d
5 changed files with 580 additions and 1 deletions

348
flappybird/app.js Normal file
View file

@ -0,0 +1,348 @@
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const fileselect = document.getElementById("fileselect");
const avatarPreview = document.getElementById("avatarpreview");
const loginScreen = document.getElementById("login");
const username = document.getElementById("username");
const playButton = document.getElementById("playbutton");
let socket = new WebSocket("ws://yasyasgo.moe/ws");
let socketReady = false;
let ingame = false;
let dead = false;
const scale = 64;
const gravity = 0.1;
const liftingForce = -17;
let velocity = 0;
let players = {};
let pillars = [];
let gameState = {};
let myId = 0;
let score = 0;
let topScore = 0;
const TriState = {
MISS: 0,
COLLIDED: 1,
PASSED: 2
};
socket.onopen = () => {
console.log("websocket connection established")
socketReady = true;
};
function socketError() {
alert("websocket connection closed");
socketReady = false;
}
socket.onerror = socketError;
socket.onclose = socketError;
socket.onmessage = (e) => {
let args = e.data.split("=");
let cmd = args.shift();
switch(cmd) {
case "self_id":
let id = parseInt(args[0]);
myId = id;
break;
case "sync_players":
let p = args.join("=");
players = JSON.parse(p);
break;
case "client_join":
let p1 = JSON.parse(args.join("="));
if(!p1.player.logged_in) return;
if(p1.id === myId) {
loginScreen.style.display = 'none';
ingame = true;
}
players[p1.id] = p1.player;
break;
case "client_left":
let id1 = parseInt(args[0]);
delete players[id1];
break;
case "gamestate":
let gs = JSON.parse(args.join("="));
gameState = gs;
topScore = 0;
dead = false;
break;
case "pos":
if(!ingame) return;
let ps = JSON.parse(args.join("="));
if(ps.id === myId) return;
players[ps.id].pos = ps.pos;
break;
case "pillars":
pillars = JSON.parse(args.join("="));
break;
case "score":
let ns = JSON.parse(args.join("="));
players[ns.id].score = ns.score;
break;
default:
console.log(`Unknown command: ${cmd}`)
}
};
function avatarchange(input) {
const file = input.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onerror = (e) => {
alert(`Error while uploading file: ${e}`)
};
reader.onload = () => {
avatarPreview.src = reader.result;
};
}
function play() {
if(username.value === "") {
alert("you need to set a username");
return;
}
if(!socketReady) {
alert("websocket is not connected. try reloading.");
return;
}
let player = {
"username": username.value,
"avatar": avatarPreview.src != "https://media.floofy.city/yasyasgo.webp" ? avatarPreview.src : ""
};
playButton.innerHTML = "Joining..."
socket.send(`player=${JSON.stringify(player)}`);
}
function updateDrawPos(player) {
if(typeof players[player].drawPos === "undefined")
players[player].drawPos = {};
players[player].drawPos.x = players[player].pos.x - players[myId].pos.x + scale;
players[player].drawPos.y = canvas.height / 2 + (players[player].pos.y * canvas.height) - scale / 2;
}
function updateAvatarObj(player) {
if(typeof players[player].avatarObj === "undefined") {
let img = document.createElement('img');
img.src = players[player].avatar != "" ? players[player].avatar : "https://media.floofy.city/yasyasgo.webp";
players[player].avatarObj = img;
}
}
function getLeaderboard() {
let keys = Object.keys(players);
for(let i = 0; i < keys.length; i++) {
if(!players[keys[i]].logged_in) keys.splice(i, 1);
}
let sorted = keys.sort((a, b) => {
if(players[a].score > players[b].score) {
return -1;
} else if (players[b].score > players[a].score) {
return 1;
}
return 0;
});
return sorted;
}
function drawPlayers() {
ctx.font = `bold ${Math.round(scale / 4)}px sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillStyle = "#000";
for(let player in players) {
if(!players[player].logged_in) continue;
updateAvatarObj(player);
if(player != myId) updateDrawPos(player);
if(players[player].drawPos.x > canvas.width || players[player].drawPos.x + scale < 0) continue;
ctx.drawImage(players[player].avatarObj, players[player].drawPos.x, players[player].drawPos.y, scale, scale);
ctx.fillText(players[player].username, players[player].drawPos.x + scale / 2, players[player].drawPos.y - scale / 8);
}
}
function drawPillars() {
for(let i = 0; i < pillars.length; i++) {
let x = pillars[i].x - players[myId].pos.x;
if(x > canvas.width || x + scale < 0) continue;
ctx.fillStyle = "#04b70e";
let topHeight = pillars[i].y * canvas.height;
ctx.fillRect(x, 0, scale, topHeight);
let bottomY = topHeight + pillars[i].height;
ctx.fillRect(x, bottomY, scale, canvas.height - bottomY);
}
}
function collidesWithPillar() {
for(let i = 0; i < pillars.length; i++) {
let x = pillars[i].x - players[myId].pos.x;
if(x > canvas.width || x + scale < 0) continue;
if(players[myId].drawPos.x > x + scale || players[myId].drawPos.x + scale < x) continue;
let topHeight = pillars[i].y * canvas.height;
let bottomY = topHeight + pillars[i].height;
if(players[myId].drawPos.y < topHeight || players[myId].drawPos.y + scale > bottomY) return TriState.COLLIDED;
return TriState.PASSED;
}
return TriState.MISS;
}
let lastCalled = Date.now();
let lastCollides = TriState.MISS;
function drawCanvas() {
let lC = Date.now();
let delta = lC - lastCalled;
if(ingame) {
if(gameState.running) {
updateDrawPos(myId);
let collide = collidesWithPillar();
if(collide === TriState.COLLIDED) dead = true;
if(lastCollides === TriState.PASSED && collide === TriState.MISS) {
score += 1;
if(score > topScore) {
topScore = score;
}
socket.send(`score=${topScore}`);
}
lastCollides = collide;
if(!dead) {
dead = players[myId].drawPos.y >= canvas.height - scale
players[myId].pos.x += 0.01 * scale * delta;
velocity += gravity * delta;
players[myId].pos.y += velocity / canvas.height;
} else if(players[myId].drawPos.y > canvas.height - scale) {
score = 0;
players[myId].pos.y = (canvas.height / 2 - scale / 2) / canvas.height;
}
socket.send(`update_pos=${JSON.stringify(players[myId].pos)}`);
ctx.fillStyle = "#8de5ff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPlayers();
drawPillars();
ctx.fillStyle = "#000";
ctx.font = "bold 30px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText(`${Math.floor((gameState.ends_at - Date.now()) / 1000) + 1}s left`, 4, 4);
let x = canvas.width - 300;
ctx.font = `bold ${scale / 3.5}px sans-serif`;
ctx.textBaseline = "middle";
let lb = getLeaderboard();
for(let i = 0; i < Math.min(lb.length, 5); i++) {
let y1 = i * (scale / 2 + 8);
ctx.fillStyle = i % 2 == 0 ? "#rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.65)";
ctx.fillRect(x, y1, 300, scale / 2 + 8);
ctx.drawImage(players[lb[i]].avatarObj, x + 4, y1 + 4, scale / 2, scale / 2);
ctx.fillStyle = lb[i] == myId ? "#ffe571" : "#fff";
ctx.textAlign = "left";
ctx.fillText(`${i + 1}. ${players[lb[i]].username}`, x + scale / 2 + 12, y1 + 6 + scale / 4);
ctx.textAlign = "right";
ctx.fillText(players[lb[i]].score, canvas.width - 4, y1 + 6 + scale / 4);
}
if(lb.indexOf(myId) > 4) {
let i = Math.min(lb.length, 5) + 1;
let y1 = i * (scale / 2 + 8);
ctx.fillStyle = i % 2 == 0 ? "#000" : "rgba(0, 0, 0, 0.75)";
ctx.fillRect(x, y1, 300, scale / 2 + 8);
ctx.drawImage(players[myId].avatarObj, x + 4, y1 + 4, scale / 2, scale / 2);
ctx.fillStyle = "#ff7e71";
ctx.textAlign = "left";
ctx.fillText(`${i + 1}. ${players[myId].username}`, x + scale / 2 + 12, y1 + 6 + scale / 4);
ctx.textAlign = "right";
ctx.fillText(players[myId].score, canvas.width - 4, y1 + 6 + scale / 4);
}
} else {
ctx.fillStyle = "#d3ff8d";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = "bold 50px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillStyle = "#000";
ctx.fillText("Leaderboard", canvas.width / 2, 50);
if(gameState.starts_at >= Date.now()) {
ctx.font = "bold 30px sans-serif";
ctx.textAlign = "left";
ctx.fillText(`Game starts in ${Math.floor((gameState.starts_at - Date.now()) / 1000) + 1}`, 4, 4);
}
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
let w = Math.max(canvas.width / 1.5, ctx.measureText("Leaderboard").width + 50);
let x = canvas.width / 2 - w / 2;
let y = 125;
ctx.fillRect(x, y, w, canvas.height - y);
ctx.font = `bold ${scale / 3.5}px sans-serif`;
ctx.textBaseline = "middle";
let lb = getLeaderboard()
for(let i = 0; i < lb.length; i++) {
let y1 = y + i * (scale / 2 + 8);
ctx.fillStyle = i % 2 == 0 ? "#000" : "rgba(0, 0, 0, 0.5)";
ctx.fillRect(x, y1, w, scale / 2 + 8);
updateAvatarObj(lb[i]);
ctx.drawImage(players[lb[i]].avatarObj, x + 4, y1 + 4, scale / 2, scale / 2);
ctx.fillStyle = lb[i] == myId ? "#ffe571" : "#fff";
ctx.textAlign = "left";
ctx.fillText(players[lb[i]].username, x + scale / 2 + 12, y1 + 6 + scale / 4);
ctx.textAlign = "right";
ctx.fillText(players[lb[i]].score, x + w - 4, y1 + 6 + scale / 4);
}
}
}
lastCalled = lC;
requestAnimationFrame(drawCanvas)
}
function lift() {
if(!ingame) return;
if(dead) {
score = 0;
players[myId].pos.x = 0;
players[myId].pos.y = 0;
dead = false;
}
velocity = liftingForce;
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
drawCanvas();
addEventListener("resize", resizeCanvas);
addEventListener("click", lift);
addEventListener("keydown", lift);

26
flappybird/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flappy Bird</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="style.css" rel="stylesheet">
<script src="app.js" defer></script>
</head>
<body>
<div class="game">
<canvas id="canvas" width="300" height="300"></canvas>
</div>
<div class="login" id="login">
<h1>Yassie Bird</h1>
<div class="avatar">
<img id="avatarpreview" src="https://media.floofy.city/yasyasgo.webp" width="100" height="100">
<button onclick="fileselect.click()">Upload custom avatar</button>
<input type="file" onchange="avatarchange(this)" id="fileselect">
</div>
<input id="username" type="text" placeholder="username">
<button id="playbutton" onclick="play()">Play</button>
</div>
</body>
</html>

155
flappybird/server/server.py Executable file
View file

@ -0,0 +1,155 @@
#!/usr/bin/env python3
import logging
import json
import threading
import time
import random
from websocket_server import WebsocketServer
players = {}
pillars = []
gamestate = {
"running": False,
"ends_at": 0,
"starts_at": 0
}
def get_time():
return round(time.time() * 1000)
def generate_pillars():
global pillars
pillars = []
for _ in range(1000):
pillar = {}
if len(pillars) == 0:
pillar = {
"x": 1200,
"y": max(0, random.random() - 0.2),
"height": random.randint(150, 350)
}
else:
pillar = {
"x": pillars[-1]["x"] + random.randint(500, 1000),
"y": max(0, random.random() - 0.2),
"height": random.randint(150, 350)
}
pillars.append(pillar)
server.send_message_to_all(f"pillars={json.dumps(pillars)}")
def reset_positions():
for player in players:
players[player]["pos"] = {
"x": 0,
"y": 0
}
server.send_message_to_all(f"sync_players={json.dumps(players)}")
def reset_scores():
for player in players:
players[player]["score"] = 0
server.send_message_to_all(f"sync_players={json.dumps(players)}")
def game_manager():
while True:
pl = []
for player in players:
if not players[player]["logged_in"]:
continue
pl.append(player)
current_time = get_time()
if len(pl) > 0:
if (not gamestate["running"]):
if gamestate["ends_at"] < current_time:
gamestate["starts_at"] = current_time + 10000
gamestate["ends_at"] = gamestate["starts_at"] + random.randint(150, 300) * 1000
server.send_message_to_all(f"gamestate={json.dumps(gamestate)}")
print(f"game starting in 10s (current time: {current_time}, starting at: {gamestate["starts_at"]})")
elif current_time >= gamestate["starts_at"]:
gamestate["running"] = True
generate_pillars()
reset_scores()
server.send_message_to_all(f"gamestate={json.dumps(gamestate)}")
print("game started")
elif get_time() >= gamestate["ends_at"]:
gamestate["running"] = False
reset_positions()
print("game stopped because time expired")
elif gamestate["running"]:
gamestate["running"] = False
gamestate["starts_at"] = get_time() - 1000
gamestate["ends_at"] = gamestate["starts_at"]
reset_positions()
print("game stopped because all players left")
time.sleep(0.1)
def new_client(client, server):
global pillars, powerups
print(f"client {client["id"]} joined")
players[client["id"]] = {
"avatar": "",
"username": "default",
"score": 0,
"pos": {
"x": 0,
"y": 0
},
"logged_in": False
}
server.send_message(client, f"self_id={client["id"]}")
server.send_message(client, f"gamestate={json.dumps(gamestate)}")
server.send_message(client, f"pillars={json.dumps(pillars)}")
def client_left(client, server):
print(f"client {client["id"]} ({players[client["id"]]["username"]}) left")
players.pop(client["id"])
server.send_message_to_all(f"client_left={client["id"]}")
def message_received(client, server, message):
args = message.split("=")
cmd = args.pop(0)
if cmd == "player":
json_str = "=".join(args)
obj = json.loads(json_str)
players[client["id"]]["avatar"] = obj["avatar"]
players[client["id"]]["username"] = obj["username"]
players[client["id"]]["logged_in"] = True
brc = {
"id": client["id"],
"player": players[client["id"]]
}
server.send_message_to_all(f"client_join={json.dumps(brc)}")
server.send_message(client, f"sync_players={json.dumps(players)}")
elif cmd == "update_pos":
json_str = "=".join(args)
obj = json.loads(json_str)
players[client["id"]]["pos"] = obj
brc = {
"id": client["id"],
"pos": obj
}
server.send_message_to_all(f"pos={json.dumps(brc)}");
elif cmd == "score":
new_score = int("=".join(args))
players[client["id"]]["score"] = new_score
brc = {
"id": client["id"],
"score": new_score
}
server.send_message_to_all(f"score={json.dumps(brc)}");
t = threading.Thread(target=game_manager)
t.start()
server = WebsocketServer(host='0.0.0.0', port=1738, loglevel=logging.INFO)
server.set_fn_new_client(new_client)
server.set_fn_client_left(client_left)
server.set_fn_message_received(message_received)
server.run_forever()

41
flappybird/style.css Normal file
View file

@ -0,0 +1,41 @@
.login {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
display: flex;
flex-direction: column;
}
.avatar {
display: flex;
flex-direction: column;
margin-bottom: 2em;
}
#avatarpreview {
margin: auto;
margin-bottom: 1em;
}
.login input {
margin-bottom: 0.5em;
}
#fileselect {
display: none;
}
button {
cursor: pointer;
}
* {
overflow: hidden;
}
body {
margin: 0;
background-color: #ffd4ba;
}

View file

@ -8,8 +8,17 @@
</head>
<body>
<div class="logo">
<img src="https://media.floofy.city/yasyasgo.webp" alt="YasYasGo Logo">
<img src="https://media.floofy.city/yasyasgo.webp" width="300" height="300" alt="YasYasGo Logo">
<h1>YasYasGo</h1>
<p>
Welcome to YasYasGo!
</p>
<ul>
<li><a href="/clicker">Yassie Clicker</a></li>
<li><a href="/flappybird">Flappy Bird clone</a></li>
</ul>
</div>
</body>
</html>