Avatar

maxm

🕳️
36 public vals
Joined January 12, 2023

Static Chess

chess

Check it out here: https://chess.maxmcd.com

Plain, brutalist, no bloat chess. Every page is only html and css. Every chess move is made by clicking a link. Send a link to your friend and they'll send you one back to make your move. No silly animations or slick interactivity to trip up your gameplay. When Google indexes this site will we successfully compute all possible chess moves?

Functionality is quite limited, and things might be broken. Please let me know if you find bugs!

Inspired by this HN discussion about sites that have all possible game states of tic-tac-toe.

I plan on extending this to support real gameplay. I think it could be a nice simple interface for long form games with friends. Might also be fun to add a static AI to play against. Feel free to PR any changes if you'd like to see something added.

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
/** @jsxImportSource https://esm.sh/react */
import { Chess, Move, Square } from "npm:chess.js";
import minify from "npm:css-simple-minifier";
import { renderToString } from "npm:react-dom/server";
class StaticChess {
size = 8;
rows = Array.from({ length: this.size }, (_, i) => i);
squares = Array.from({ length: this.size }, (_, i) => i);
constructor() {}
async fetch(req: Request): Promise<Response> {
const gameInfo = parseURL(req.url);
if (gameInfo === undefined) {
return new Response("Not Found", { status: 404, headers: { "cache-control": "max-age=86400, public" } });
}
const game = new Game(gameInfo.game, gameInfo.selected);
return new Response(
renderToString(
<html>
<head>
<title>Static Chess</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://fav.farm/♟️" />
<style>{minify(CSS)}</style>
</head>
<body>
<h1>Static Chess</h1>
<div>
<a href="https://www.val.town/v/maxm/staticChess">info</a> - <a href="/">reset</a>
</div>
<div className="board">
{this.rows.map(row => (
<div key={row} className="row">{this.squares.map(square => game.squareContent(row, square))}</div>
))}
</div>
<div className="info">
{game.selected
? "Click a highted square to move the selected piece, or select a different piece."
: `It is ${{ w: "white", b: "black" }[game.game.turn()]}'s turn. Click a piece to make a move.`}
</div>
</body>
</html>,
),
{ headers: { "content-type": "text/html", "cache-control": "max-age=86400, public" } },
);
}
}
class Game {
game: Chess;
selected?: string;
selectable: string[];
board;
nextMoves: { [key: string]: Move };
fen: string;
constructor(game: Chess, selected?: string) {
this.game = game;
this.selected = selected;
this.board = game.board();
this.fen = game.fen().replaceAll(" ", "_");
this.nextMoves = {};
this.selectable = game.moves({ verbose: true }).map((m) => m.from.toString());
if (this.selected) {
var moves = game.moves({
square: selected as Square,
verbose: true,
});
for (const move of moves) {
this.nextMoves[move.to] = move;
}
}
}
squareContent(row: number, square: number) {
const pos = indexToPos(row, square);
const color = this.board[row][square]?.color;
let className = "square";
if (color) className += " " + color;
if (this.selected) {
if (this.nextMoves[pos]) className += " highlight";
if (this.selected == pos) className += " selected highlight";
}
const squareContent = (() => {
if (this.selectable.includes(pos)) {
return <a href={`/${this.fen}/${pos}`}>{pieces[this.board[row][square]?.type]}</a>;
}
const nextMove = this.nextMoves[pos];
if (nextMove !== undefined) {
return (
<a href={`/${nextMove.after.replaceAll(" ", "_")}/`}>
{pieces[this.board[row][square]?.type]}
</a>
);
}
return <span>{pieces[this.board[row][square]?.type]}</span>;
})();

Render a PNG

This tiny smiley face is rendered by this val:

The image response is cached by the CDN, so make sure to change the image name if you make code changes. Any name with a .png extension will work: https://maxm-smileypng.web.val.run/-.png

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
export const smileyPNG = async (request: Request) => {
const { encode } = await import("https://deno.land/x/pngs/mod.ts");
// Dimensions of the image
const [width, height] = [10, 10];
// Create our PNG canvas
const imageData = new Uint8Array(width * height * 4);
const y = [255, 255, 0, 255]; // Yellow
const b = [0, 0, 0, 255]; // Black
const t = [0, 0, 0, 0]; // Transparent
// dprint-ignore
const smileyFace = [
t, t, y, y, y, y, y, y, t, t,
t, y, y, y, y, y, y, y, y, t,
y, y, y, y, y, y, y, y, y, y,
y, y, b, y, y, y, y, b, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, b, y, y, y, y, b, y, y,
y, y, y, b, b, b, b, y, y, y,
t, y, y, y, y, y, y, y, y, t,
t, t, y, y, y, y, y, y, t, t,
];
// Move around the bytes and encode the image
const smileyFaceData = [].concat(...smileyFace);
for (let i = 0; i < width * height * 4; i++) {
imageData[i] = smileyFaceData[i];
}
const png = encode(imageData, 10, 10);
return new Response(png, { headers: { "Content-Type": "image/png" } });
};

Self Editing Website

Visit and edit at: https://maxm-selfeditingwebsite.web.val.run/

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
import { set } from "https://esm.town/v/std/set?v=11";
let { selfEditingWebsiteBody } = await import("https://esm.town/v/maxm/selfEditingWebsiteBody");
export const selfEditingWebsite = async (request: Request) => {
if (request == null) {
return "not invoked correctly";
}
// Otherwise, someone has submitted a form so let's handle that
if (selfEditingWebsiteBody === undefined) {
let defaultBody = `<h1>Edit Me!</h1>`;
selfEditingWebsiteBody = defaultBody;
}
if (request.method == "POST") {
let formData = await request.formData();
if (formData.get("source") !== undefined) {
selfEditingWebsiteBody = formData.get("source").toString();
await set("selfEditingWebsiteBody", selfEditingWebsiteBody);
}
}
let body = selfEditingWebsiteBody;
const form = `
<div style="border: 1px solid #999;">
<form action="" method="POST">
<label for="source">Edit the source of this web page:</label>
<textarea id="source" name="source" required>${body}</textarea>
<br>
<input type="submit" value="Submit">
</form>
</div>`;
return new Response(
`<!DOCTYPE html>
<html>
<head>
<title>I am a self-editing website!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.js" integrity="sha512-2359y3bpxFfJ9xZw1r2IHM0WlZjZLI8gjLuhTGOVtRPzro3dOFy4AyEEl9ECwVbQ/riLXMeCNy0h6HMt2WUtYw==" crossorigin="anonymous" referrerpolicy="no-referrer"></s
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.15/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy=
</head>
<body>
${body}
${form}
<script>
var editor = CodeMirror.fromTextArea(document.getElementById('source'), {
mode: "html",
lineNumbers: true,
});
editor.save();
</script>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } },
);
};

A Go http handler running in Val Town:

Using this go source file, this go library, this Deno library, and this script. Image rendering is happening here. Mandelbrot rendering code taken from here.

Blog post, libraries with readmes and more info coming!

package main

import (
	"fmt"
	"net/http"

	gotown "github.com/maxmcd/go-town"
)

func main() {
	img := renderImage()
	gotown.ListenAndServe(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/mandelbrot.png" {
			w.Header().Set("Content-Type", "image/png")
			w.Write(img)
			return
		}
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `
			<head><link rel="icon" href="/mandelbrot.png"></head>
			<style>body {font-family: sans-serif}</style>
			Go-Rendered mandelbrot image served from a Go HTTP handler <a href="https://www.val.town/v/maxm/tinygoHttpExample">on Val Town</a>
			<br /><img src='/mandelbrot.png' />
		`)
	}))
}
1
2
3
4
5
6
7
8
9
import handler, { init } from "https://deno.land/x/gotown@v0.0.7/val-town-tinygo-http-example/mod.ts";
const resp = await fetch("https://deno.land/x/gotown@v0.0.7/val-town-tinygo-http-example/main.wasm");
init(new Uint8Array(await resp.arrayBuffer()));
export default async function(req: Request): Promise<Response> {
const resp = await handler(req);
resp.headers.set("Cache-Control", "max-age=3600");
return resp;
}

Retro Visit Counter

You are visitor number:

How special!

Want a retro visitor counter for your myspace page or geocities website? Fork this val and add the image link to your website:

<img src="https://maxm-retrovisitcounter.web.val.run/counter.png">
<!-- Make sure you swap this subdomain out with the subdomain of your forked val --> 
<img src="https://[CHANGE ME!].web.val.run/counter.png">
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
import { sqlite } from "https://esm.town/v/std/sqlite";
await sqlite.execute(`create table if not exists counter(
name text,
counter integer
)`);
async function getCurrentCount(): Promise<number> {
let counter = await sqlite.execute(`select counter from counter where name = 'retro'`);
if (counter.rows.length === 0) {
await sqlite.execute(`insert into counter (name, counter) values ('retro', 1)`);
}
let [currentCount] = (counter.rows[0] as unknown) as number[];
await sqlite.execute(`update counter set counter = counter + 1 where name = 'retro'`);
return currentCount;
}
export const retroVisitCounter = async (request: Request) => {
const count = await getCurrentCount();
const { encode } = await import("https://deno.land/x/pngs/mod.ts");
if (request.url.endsWith("favicon.ico"))
return new Response("not found", { status: 404 });
// Set up all of our configured constants.
const numberPixelHeight = 7;
const numberPixelWidth = 5;
const pixelMultiplier = 3;
const numberPlaces = 6;
const padding = 1;
const B = [0, 0, 0, 255]; // Black
const _ = [153, 153, 153, 255]; // Transparent
// dprint-ignore
const numbers = [
[
_, B, B, B, _,
B, _, _, _, B,
B, _, _, B, B,
B, _, B, _, B,
B, B, _, _, B,
B, _, _, _, B,
_, B, B, B, _,
],
[
_, _, B, _, _,
_, B, B, _, _,
_, _, B, _, _,
_, _, B, _, _,
_, _, B, _, _,
_, _, B, _, _,
B, B, B, B, B,
],
[
_, B, B, B, _,
B, _, _, _, B,
_, _, _, _, B,
_, _, B, B, _,
_, B, _, _, _,
B, _, _, _, _,
B, B, B, B, B,
],
[
_, B, B, B, _,
B, _, _, _, B,
_, _, _, _, B,
_, _, B, B, _,
_, _, _, _, B,
B, _, _, _, B,
_, B, B, B, _,
],
[
_, _, _, B, B,
_, _, B, _, B,
_, B, _, _, B,
B, _, _, _, B,
B, B, B, B, B,
_, _, _, _, B,
_, _, _, _, B,
],
[
B, B, B, B, B,
B, _, _, _, _,
B, B, B, B, _,
_, _, _, _, B,
_, _, _, _, B,
B, _, _, _, B,
_, B, B, B, _,
],
[
_, _, B, B, _,
_, B, _, _, _,
B, _, _, _, _,
B, B, B, B, _,
B, _, _, _, B,
B, _, _, _, B,
_, B, B, B, _,
],
[
B, B, B, B, B,
B, _, _, _, B,
_, _, _, _, B,
_, _, _, B, _,
_, _, B, _, _,
1
2
3
4
5
6
7
import { Router, json } from "npm:itty-router@4";
export const ittyRouterExample = async (request: Request) => {
const router = Router();
router.get("/", () => "Hi from itty-router!");
return router.handle(request).then(json);
};
1
2
3
export default async function (req: Request): Promise<Response> {
return Response.json({ ok: true })
}
1
2
3
4
5
6
7
8
9
10
11
import { newClient } from "https://esm.town/v/maxm/libraryThatNeedsValidation";
try {
let client = await newClient("maxm", "wrong");
} catch (err) {
console.log(err);
}
let client = await newClient("maxm", "password");
console.log("logged in and validated", client.getUsername());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const newClient = async (username: string, password: string): Client => {
const url = `https://${username}-userspaceauth.web.val.run/validate/${password}`;
let resp = await fetch(url);
if (resp.status !== 200) {
throw new Error("Invliad response. Have you set up your fork? https://www.val.town/v/maxm/userspaceauth/fork?");
}
let data = await resp.json();
if (!data) {
throw new Error("Incorrect password");
}
return new Client(username);
};
class Client {
private username: string;
constructor(username: string) {
this.username = username;
}
getUsername() {
return this.username;
}
}
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
/** @jsxImportSource https://esm.sh/preact */
import { blob } from "https://esm.town/v/std/blob";
// import * as opaque from "npm:@cloudflare/opaque-ts";
import { Hono } from "npm:hono@3";
import { render } from "npm:preact-render-to-string";
const resp = (content, status) =>
new Response(render(content), {
status,
headers: {
"Content-Type": "text/html",
},
});
const trimSuffix = (s: string, suffix: string): string => {
if (!s.endsWith(suffix)) {
return s;
}
return s.slice(0, s.length - suffix.length);
};
const getPassword = async (): Promise<string | undefined> => {
try {
let resp = await blob.get(PASSWORD_BLOB_KEY);
return await resp.text();
} catch (e) {
return undefined;
}
};
const KNOWN_HOSTNAME = ".web.val.run";
const PASSWORD_BLOB_KEY = "password"; // hah
const app = new Hono();
app.get("/", async (c) => {
const password = await getPassword();
const u = new URL(c.req.url);
if (!u.host.endsWith(KNOWN_HOSTNAME)) {
return resp((<div>Hostname does not end with {KNOWN_HOSTNAME}, cannot continue</div>), 400);
}
const userAndValName = trimSuffix(u.host, KNOWN_HOSTNAME);
const [username, valName] = userAndValName.split("-");
if (username === "") {
return resp((<div>Username not found</div>), 400);
}
if (valName !== "userspaceauth") {
return resp((<div>Val must be called "userspaceAuth"</div>), 400);
}
let content = password
? <div>Your password has been set up and this val is ready to authenticate requests for you</div>
: (
<form method="post">
<p>
Enter a password that you'll use to authenticate:
</p>
<div>
<input type="password" name="password"></input>
<button type="submit">Submit</button>
</div>
</form>
);
return resp(
(<>
<div>Username: {username}</div>
<div>Val Name: {valName}</div>
<div>{content}</div>
</>),
200,
);
});
app.post("/", async (c) => {
const fd = await c.req.formData();
const password = fd.get("password");
// TODO: more validation
if (typeof password !== "string" || password.length < 8) {
return resp((<div>Invalid password!</div>), 400);
}
await blob.set(PASSWORD_BLOB_KEY, "password");
return c.redirect("/");
});
app.get("/reset", async (c) => {
// TODO: this can be abused
await blob.delete(PASSWORD_BLOB_KEY);
});
app.post("/validate", async (c) => {
const realPassword = await getPassword();
const fd = await c.req.formData();
const password = fd.get("password");
const match = password === realPassword;
return c.json(match, match ? 200 : 401);
});
app.get("/validate/:password", async (c) => {
const realPassword = await getPassword();
const password = c.req.param("password");
const match = password === realPassword;
return c.json(match, match ? 200 : 401);
});