1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
/// <reference lib="dom" />
const ws = new WebSocket("/events")
type Packet = "tick"
| { pos: { id: number, x: number, y: number } }
| { game: { width: number, height: number } }
| { player: { id: number, name: string } }
| { die: number[] }
class Snake {
parts: { x: number, y: number, dx: number, dy: number }[] = []
color: string
dead = false
constructor(public name: string) {
this.color = name_color(name)
}
add_part(x: number, y: number) {
if (!this.parts.length) return this.parts.push({ x, y, dx: 0, dy: 0 })
const last = this.parts[this.parts.length - 1]
let dx = x - last.x, dy = y - last.y;
if (x > last.x + 1 || x < last.x - 1) dx *= -1, dx /= Math.abs(dx)
if (y > last.y + 1 || y < last.y - 1) dy *= -1, dy /= Math.abs(dy)
this.parts.push({ x, y, dx, dy })
}
}
let size = 0
let tick_anim = 0;
const snakes = new Map<number, Snake>()
let canvas: HTMLCanvasElement
let ctx: CanvasRenderingContext2D
document.addEventListener("DOMContentLoaded", () => {
canvas = document.createElement("canvas")
ctx = canvas.getContext("2d")!
document.body.append(canvas)
canvas.width = 1000;
canvas.height = 1000;
redraw()
})
function redraw() {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save()
const scale = canvas.width / size
ctx.scale(scale, scale)
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
ctx.strokeStyle = "grey"
ctx.lineWidth = 0.02
ctx.strokeRect(x, y, 1, 1)
}
}
ctx.translate(0.5, 0.5)
for (const [xo, yo] of [[0, 0], [-size, 0], [size, 0], [0, -size], [0, size]]) {
ctx.save()
ctx.translate(xo, yo)
for (const snake of snakes.values()) {
ctx.beginPath();
for (let i = 0; i < snake.parts.length; i++) {
const p = snake.parts[i];
ctx.moveTo(p.x - p.dx, p.y - p.dy)
if (i == snake.parts.length - 1)
ctx.lineTo(tick_lerp(p.x - p.dx, p.x), tick_lerp(p.y - p.dy, p.y))
else ctx.lineTo(p.x, p.y)
}
ctx.lineCap = "round"
ctx.lineJoin = "round"
if (snake.dead) ctx.lineWidth = tick_lerp(0.6, 0.001)
else ctx.lineWidth = 0.6;
ctx.strokeStyle = snake.color
ctx.stroke()
}
ctx.restore()
}
ctx.restore()
ctx.save()
ctx.translate(scale / 2, scale / 4)
for (const [xo, yo] of [[0, 0], [-size * scale, 0], [size * scale, 0], [0, -size * scale], [0, size * scale]]) {
ctx.save()
ctx.translate(xo, yo)
for (const snake of snakes.values()) {
const p = snake.parts[snake.parts.length - 1];
if (!p) continue
ctx.font = "30px sans-serif"
if (snake.dead) ctx.fillStyle = `rgba(1,1,1,${tick_lerp(1, 0)})`
else ctx.fillStyle = "white"
ctx.textAlign = "center"
ctx.textBaseline = "bottom"
ctx.fillText(snake.name, tick_lerp(p.x - p.dx, p.x) * scale, tick_lerp(p.y - p.dy, p.y) * scale)
}
ctx.restore()
}
ctx.restore()
tick_anim += 0.1
tick_anim = Math.min(1, tick_anim)
requestAnimationFrame(redraw)
}
function tick_lerp(f: number, t: number) {
return (1 - tick_anim) * f + tick_anim * t
}
function name_color(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash += name.charCodeAt(i);
hash |= 0;
hash ^= hash << 13;
hash ^= hash >> 17;
hash ^= hash << 5;
}
return `hsl(${hash % 360}deg 80% 40%)`;
}
ws.onerror = console.error
ws.onmessage = message => {
const p = JSON.parse(message.data) as Packet
console.log(p);
if (p == "tick") {
tick_anim = 0
const d = []
for (const [k, s] of snakes) {
if (s.dead) d.push(k)
}
d.forEach(k => snakes.delete(k))
} else if ("game" in p) {
snakes.clear()
size = p.game.width
} else if ("player" in p) {
snakes.set(p.player.id, new Snake(p.player.name))
} else if ("pos" in p) {
snakes.get(p.pos.id)?.add_part(p.pos.x, p.pos.y)
} else if ("die" in p) {
for (const d of p.die) {
const s = snakes.get(d)
if (s) s.dead = true
}
}
}
|