Plan 9 from Bell Labs’s /usr/web/sources/contrib/ericvh/go-plan9/src/pkg/exp/4s/xs.go

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


// games/4s - a tetris clone
//
// Derived from Plan 9's /sys/src/games/xs.c
// http://plan9.bell-labs.com/sources/plan9/sys/src/games/xs.c
//
// Copyright (C) 2003, Lucent Technologies Inc. and others. All Rights Reserved.
// Portions Copyright 2009 The Go Authors.  All Rights Reserved.
// Distributed under the terms of the Lucent Public License Version 1.02
// See http://plan9.bell-labs.com/plan9/license.html

/*
 * engine for 4s, 5s, etc
 */

package main

import (
	"exp/draw";
	"image";
	"log";
	"os";
	"rand";
	"time";
)

/*
Cursor whitearrow = {
	{0, 0},
	{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC,
	 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC,
	 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC,
	 0xF3, 0xF8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, },
	{0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0xC0, 0x1C,
	 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x38, 0xC0, 0x1C,
	 0xC0, 0x0E, 0xC0, 0x07, 0xCE, 0x0E, 0xDF, 0x1C,
	 0xD3, 0xB8, 0xF1, 0xF0, 0xE0, 0xE0, 0xC0, 0x40, }
};
*/

const (
	CNone	= 0;
	CBounds	= 1;
	CPiece	= 2;
	NX	= 10;
	NY	= 20;

	NCOL	= 10;

	MAXN	= 5;
)

var (
	N				int;
	display				draw.Context;
	screen				draw.Image;
	screenr				draw.Rectangle;
	board				[NY][NX]byte;
	rboard				draw.Rectangle;
	pscore				draw.Point;
	scoresz				draw.Point;
	pcsz				= 32;
	pos				draw.Point;
	bbr, bb2r			draw.Rectangle;
	bb, bbmask, bb2, bb2mask	*image.RGBA;
	whitemask			image.Image;
	br, br2				draw.Rectangle;
	points				int;
	dt				int;
	DY				int;
	DMOUSE				int;
	lastmx				int;
	mouse				draw.Mouse;
	newscreen			bool;
	timerc				<-chan int64;
	suspc				chan bool;
	mousec				chan draw.Mouse;
	resizec				<-chan bool;
	kbdc				chan int;
	suspended			bool;
	tsleep				int;
	piece				*Piece;
	pieces				[]Piece;
)

type Piece struct {
	rot	int;
	tx	int;
	sz	draw.Point;
	d	[]draw.Point;
	left	*Piece;
	right	*Piece;
}

var txbits = [NCOL][32]byte{
	[32]byte{0xDD, 0xDD, 0xFF, 0xFF, 0x77, 0x77, 0xFF, 0xFF,
		0xDD, 0xDD, 0xFF, 0xFF, 0x77, 0x77, 0xFF, 0xFF,
		0xDD, 0xDD, 0xFF, 0xFF, 0x77, 0x77, 0xFF, 0xFF,
		0xDD, 0xDD, 0xFF, 0xFF, 0x77, 0x77, 0xFF, 0xFF,
	},
	[32]byte{0xDD, 0xDD, 0x77, 0x77, 0xDD, 0xDD, 0x77, 0x77,
		0xDD, 0xDD, 0x77, 0x77, 0xDD, 0xDD, 0x77, 0x77,
		0xDD, 0xDD, 0x77, 0x77, 0xDD, 0xDD, 0x77, 0x77,
		0xDD, 0xDD, 0x77, 0x77, 0xDD, 0xDD, 0x77, 0x77,
	},
	[32]byte{0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
	},
	[32]byte{0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
		0xAA, 0xAA, 0x55, 0x55, 0xAA, 0xAA, 0x55, 0x55,
	},
	[32]byte{0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88,
		0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88,
		0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88,
		0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88,
	},
	[32]byte{0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00,
		0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00,
		0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00,
		0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00,
	},
	[32]byte{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
	},
	[32]byte{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
		0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
	},
	[32]byte{0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
		0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
		0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
		0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
	},
	[32]byte{0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0x33, 0x33, 0x33,
		0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0x33, 0x33, 0x33,
		0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0x33, 0x33, 0x33,
		0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0x33, 0x33, 0x33,
	},
}

var txpix = [NCOL]draw.Color{
	draw.Yellow,		/* yellow */
	draw.Cyan,		/* cyan */
	draw.Green,		/* lime green */
	draw.GreyBlue,		/* slate */
	draw.Red,		/* red */
	draw.GreyGreen,		/* olive green */
	draw.Blue,		/* blue */
	draw.Color(0xFF55AAFF),	/* pink */
	draw.Color(0xFFAAFFFF),	/* lavender */
	draw.Color(0xBB005DFF),	/* maroon */
}

func movemouse() int {
	//mouse.draw.Point = draw.Pt(rboard.Min.X + rboard.Dx()/2, rboard.Min.Y + rboard.Dy()/2);
	//moveto(mousectl, mouse.Xy);
	return mouse.X
}

func warp(p draw.Point, x int) int {
	if !suspended && piece != nil {
		x = pos.X + piece.sz.X*pcsz/2;
		if p.Y < rboard.Min.Y {
			p.Y = rboard.Min.Y
		}
		if p.Y >= rboard.Max.Y {
			p.Y = rboard.Max.Y - 1
		}
		//moveto(mousectl, draw.Pt(x, p.Y));
	}
	return x;
}

func initPieces() {
	for i := range pieces {
		p := &pieces[i];
		if p.rot == 3 {
			p.right = &pieces[i-3]
		} else {
			p.right = &pieces[i+1]
		}
		if p.rot == 0 {
			p.left = &pieces[i+3]
		} else {
			p.left = &pieces[i-1]
		}
	}
}

func collide(pt draw.Point, p *Piece) bool {
	pt.X = (pt.X - rboard.Min.X) / pcsz;
	pt.Y = (pt.Y - rboard.Min.Y) / pcsz;
	for _, q := range p.d {
		pt.X += q.X;
		pt.Y += q.Y;
		if pt.X < 0 || pt.X >= NX || pt.Y < 0 || pt.Y >= NY {
			return true;
			continue;
		}
		if board[pt.Y][pt.X] != 0 {
			return true
		}
	}
	return false;
}

func collider(pt, pmax draw.Point) bool {
	pi := (pt.X - rboard.Min.X) / pcsz;
	pj := (pt.Y - rboard.Min.Y) / pcsz;
	n := pmax.X / pcsz;
	m := pmax.Y/pcsz + 1;
	for i := pi; i < pi+n && i < NX; i++ {
		for j := pj; j < pj+m && j < NY; j++ {
			if board[j][i] != 0 {
				return true
			}
		}
	}
	return false;
}

func setpiece(p *Piece) {
	draw.Draw(bb, bbr, draw.White, nil, draw.ZP);
	draw.Draw(bbmask, bbr, draw.Transparent, nil, draw.ZP);
	br = draw.Rect(0, 0, 0, 0);
	br2 = br;
	piece = p;
	if p == nil {
		return
	}
	var op draw.Point;
	var r draw.Rectangle;
	r.Min = bbr.Min;
	for i, pt := range p.d {
		r.Min.X += pt.X * pcsz;
		r.Min.Y += pt.Y * pcsz;
		r.Max.X = r.Min.X + pcsz;
		r.Max.Y = r.Min.Y + pcsz;
		if i == 0 {
			draw.Draw(bb, r, draw.Black, nil, draw.ZP);
			draw.Draw(bb, r.Inset(1), txpix[piece.tx], nil, draw.ZP);
			draw.Draw(bbmask, r, draw.Opaque, nil, draw.ZP);
			op = r.Min;
		} else {
			draw.Draw(bb, r, bb, nil, op);
			draw.Draw(bbmask, r, bbmask, nil, op);
		}
		if br.Max.X < r.Max.X {
			br.Max.X = r.Max.X
		}
		if br.Max.Y < r.Max.Y {
			br.Max.Y = r.Max.Y
		}
	}
	br.Max = br.Max.Sub(bbr.Min);
	delta := draw.Pt(0, DY);
	br2.Max = br.Max.Add(delta);
	r = br.Add(bb2r.Min);
	r2 := br2.Add(bb2r.Min);
	draw.Draw(bb2, r2, draw.White, nil, draw.ZP);
	draw.Draw(bb2, r.Add(delta), bb, nil, bbr.Min);
	draw.Draw(bb2mask, r2, draw.Transparent, nil, draw.ZP);
	draw.Draw(bb2mask, r, draw.Opaque, bbmask, bbr.Min);
	draw.Draw(bb2mask, r.Add(delta), draw.Opaque, bbmask, bbr.Min);
}

func drawpiece() {
	draw.Draw(screen, br.Add(pos), bb, bbmask, bbr.Min);
	if suspended {
		draw.Draw(screen, br.Add(pos), draw.White, whitemask, draw.ZP)
	}
}

func undrawpiece() {
	var mask image.Image;
	if collider(pos, br.Max) {
		mask = bbmask
	}
	draw.Draw(screen, br.Add(pos), draw.White, mask, bbr.Min);
}

func rest() {
	pt := pos.Sub(rboard.Min).Div(pcsz);
	for _, p := range piece.d {
		pt.X += p.X;
		pt.Y += p.Y;
		board[pt.Y][pt.X] = byte(piece.tx + 16);
	}
}

func canfit(p *Piece) bool {
	var dx = [...]int{0, -1, 1, -2, 2, -3, 3, 4, -4};
	j := N + 1;
	if j >= 4 {
		j = p.sz.X;
		if j < p.sz.Y {
			j = p.sz.Y
		}
		j = 2*j - 1;
	}
	for i := 0; i < j; i++ {
		var z draw.Point;
		z.X = pos.X + dx[i]*pcsz;
		z.Y = pos.Y;
		if !collide(z, p) {
			z.Y = pos.Y + pcsz - 1;
			if !collide(z, p) {
				undrawpiece();
				pos.X = z.X;
				return true;
			}
		}
	}
	return false;
}

func score(p int) {
	points += p
	//	snprint(buf, sizeof(buf), "%.6ld", points);
	//	draw.Draw(screen, draw.Rpt(pscore, pscore.Add(scoresz)), draw.White, nil, draw.ZP);
	//	string(screen, pscore, draw.Black, draw.ZP, font, buf);
}

func drawsq(b draw.Image, p draw.Point, ptx int) {
	var r draw.Rectangle;
	r.Min = p;
	r.Max.X = r.Min.X + pcsz;
	r.Max.Y = r.Min.Y + pcsz;
	draw.Draw(b, r, draw.Black, nil, draw.ZP);
	draw.Draw(b, r.Inset(1), txpix[ptx], nil, draw.ZP);
}

func drawboard() {
	draw.Border(screen, rboard.Inset(-2), 2, draw.Black, draw.ZP);
	draw.Draw(screen, draw.Rect(rboard.Min.X, rboard.Min.Y-2, rboard.Max.X, rboard.Min.Y),
		draw.White, nil, draw.ZP);
	for i := 0; i < NY; i++ {
		for j := 0; j < NX; j++ {
			if board[i][j] != 0 {
				drawsq(screen, draw.Pt(rboard.Min.X+j*pcsz, rboard.Min.Y+i*pcsz), int(board[i][j]-16))
			}
		}
	}
	score(0);
	if suspended {
		draw.Draw(screen, screenr, draw.White, whitemask, draw.ZP)
	}
}

func choosepiece() {
	for {
		i := rand.Intn(len(pieces));
		setpiece(&pieces[i]);
		pos = rboard.Min;
		pos.X += rand.Intn(NX) * pcsz;
		if !collide(draw.Pt(pos.X, pos.Y+pcsz-DY), piece) {
			break
		}
	}
	drawpiece();
	display.FlushImage();
}

func movepiece() bool {
	var mask image.Image;
	if collide(draw.Pt(pos.X, pos.Y+pcsz), piece) {
		return false
	}
	if collider(pos, br2.Max) {
		mask = bb2mask
	}
	draw.Draw(screen, br2.Add(pos), bb2, mask, bb2r.Min);
	pos.Y += DY;
	display.FlushImage();
	return true;
}

func suspend(s bool) {
	suspended = s;
	/*
		if suspended {
			setcursor(mousectl, &whitearrow);
		} else {
			setcursor(mousectl, nil);
		}
	*/
	if !suspended {
		drawpiece()
	}
	drawboard();
	display.FlushImage();
}

func pause(t int) {
	display.FlushImage();
	for {
		select {
		case s := <-suspc:
			if !suspended && s {
				suspend(true)
			} else if suspended && !s {
				suspend(false);
				lastmx = warp(mouse.Point, lastmx);
			}
		case <-timerc:
			if suspended {
				break
			}
			t -= tsleep;
			if t < 0 {
				return
			}
		case <-resizec:
			//redraw(true);
		case mouse = <-mousec:
		case <-kbdc:
		}
	}
}

func horiz() bool {
	var lev [MAXN]int;
	h := 0;
	for i := 0; i < NY; i++ {
		for j := 0; board[i][j] != 0; j++ {
			if j == NX-1 {
				lev[h] = i;
				h++;
				break;
			}
		}
	}
	if h == 0 {
		return false
	}
	r := rboard;
	newscreen = false;
	for j := 0; j < h; j++ {
		r.Min.Y = rboard.Min.Y + lev[j]*pcsz;
		r.Max.Y = r.Min.Y + pcsz;
		draw.Draw(screen, r, draw.White, whitemask, draw.ZP);
		display.FlushImage();
	}
	PlaySound(whoosh);
	for i := 0; i < 3; i++ {
		pause(250);
		if newscreen {
			drawboard();
			break;
		}
		for j := 0; j < h; j++ {
			r.Min.Y = rboard.Min.Y + lev[j]*pcsz;
			r.Max.Y = r.Min.Y + pcsz;
			draw.Draw(screen, r, draw.White, whitemask, draw.ZP);
		}
		display.FlushImage();
	}
	r = rboard;
	for j := 0; j < h; j++ {
		i := NY - lev[j] - 1;
		score(250 + 10*i*i);
		r.Min.Y = rboard.Min.Y;
		r.Max.Y = rboard.Min.Y + lev[j]*pcsz;
		draw.Draw(screen, r.Add(draw.Pt(0, pcsz)), screen, nil, r.Min);
		r.Max.Y = rboard.Min.Y + pcsz;
		draw.Draw(screen, r, draw.White, nil, draw.ZP);
		for k := lev[j] - 1; k >= 0; k-- {
			board[k+1] = board[k]
		}
		board[0] = [NX]byte{};
	}
	display.FlushImage();
	return true;
}

func mright() {
	if !collide(draw.Pt(pos.X+pcsz, pos.Y), piece) &&
		!collide(draw.Pt(pos.X+pcsz, pos.Y+pcsz-DY), piece) {
		undrawpiece();
		pos.X += pcsz;
		drawpiece();
		display.FlushImage();
	}
}

func mleft() {
	if !collide(draw.Pt(pos.X-pcsz, pos.Y), piece) &&
		!collide(draw.Pt(pos.X-pcsz, pos.Y+pcsz-DY), piece) {
		undrawpiece();
		pos.X -= pcsz;
		drawpiece();
		display.FlushImage();
	}
}

func rright() {
	if canfit(piece.right) {
		setpiece(piece.right);
		drawpiece();
		display.FlushImage();
	}
}

func rleft() {
	if canfit(piece.left) {
		setpiece(piece.left);
		drawpiece();
		display.FlushImage();
	}
}

var fusst = 0

func drop(f bool) bool {
	if f {
		score(5 * (rboard.Max.Y - pos.Y) / pcsz);
		for movepiece() {
		}
	}
	fusst = 0;
	rest();
	if pos.Y == rboard.Min.Y && !horiz() {
		return true
	}
	horiz();
	setpiece(nil);
	pause(1500);
	choosepiece();
	lastmx = warp(mouse.Point, lastmx);
	return false;
}

func play() {
	var om draw.Mouse;
	dt = 64;
	lastmx = -1;
	lastmx = movemouse();
	choosepiece();
	lastmx = warp(mouse.Point, lastmx);
	for {
		select {
		case mouse = <-mousec:
			if suspended {
				om = mouse;
				break;
			}
			if lastmx < 0 {
				lastmx = mouse.X
			}
			if mouse.X > lastmx+DMOUSE {
				mright();
				lastmx = mouse.X;
			}
			if mouse.X < lastmx-DMOUSE {
				mleft();
				lastmx = mouse.X;
			}
			if mouse.Buttons&^om.Buttons&1 == 1 {
				rleft()
			}
			if mouse.Buttons&^om.Buttons&2 == 2 {
				if drop(true) {
					return
				}
			}
			if mouse.Buttons&^om.Buttons&4 == 4 {
				rright()
			}
			om = mouse;

		case s := <-suspc:
			if !suspended && s {
				suspend(true)
			} else if suspended && !s {
				suspend(false);
				lastmx = warp(mouse.Point, lastmx);
			}

		case <-resizec:
			//redraw(true);

		case r := <-kbdc:
			if suspended {
				break
			}
			switch r {
			case 'f', ';':
				mright()
			case 'a', 'j':
				mleft()
			case 'd', 'l':
				rright()
			case 's', 'k':
				rleft()
			case ' ':
				if drop(true) {
					return
				}
			}

		case <-timerc:
			if suspended {
				break
			}
			dt -= tsleep;
			if dt < 0 {
				i := 1;
				dt = 16 * (points + rand.Intn(10000) - 5000) / 10000;
				if dt >= 32 {
					i += (dt - 32) / 16;
					dt = 32;
				}
				dt = 52 - dt;
				for ; i > 0; i-- {
					if movepiece() {
						continue
					}
					fusst++;
					if fusst == 40 {
						if drop(false) {
							return
						}
						break;
					}
				}
			}
		}
	}
}

func suspproc() {
	mc := display.MouseChan();
	kc := display.KeyboardChan();

	s := false;
	for {
		select {
		case mouse = <-mc:
			mousec <- mouse
		case r := <-kc:
			switch r {
			case 'q', 'Q', 0x04, 0x7F:
				os.Exit(0)
			default:
				if s {
					s = false;
					suspc <- s;
					break;
				}
				switch r {
				case 'z', 'Z', 'p', 'P', 0x1B:
					s = true;
					suspc <- s;
				default:
					kbdc <- r
				}
			}
		}
	}
}

func redraw(new bool) {
	//	if new && getwindow(display, Refmesg) < 0 {
	//		sysfatal("can't reattach to window");
	//	}
	r := draw.Rect(0, 0, screen.Width(), screen.Height());
	pos.X = (pos.X - rboard.Min.X) / pcsz;
	pos.Y = (pos.Y - rboard.Min.Y) / pcsz;
	dx := r.Max.X - r.Min.X;
	dy := r.Max.Y - r.Min.Y - 2*32;
	DY = dx / NX;
	if DY > dy/NY {
		DY = dy / NY
	}
	DY /= 8;
	if DY > 4 {
		DY = 4
	}
	pcsz = DY * 8;
	DMOUSE = pcsz / 3;
	if pcsz < 8 {
		log.Exitf("screen too small: %d", pcsz)
	}
	rboard = screenr;
	rboard.Min.X += (dx - pcsz*NX) / 2;
	rboard.Min.Y += (dy-pcsz*NY)/2 + 32;
	rboard.Max.X = rboard.Min.X + NX*pcsz;
	rboard.Max.Y = rboard.Min.Y + NY*pcsz;
	pscore.X = rboard.Min.X + 8;
	pscore.Y = rboard.Min.Y - 32;
	//	scoresz = stringsize(font, "000000");
	pos.X = pos.X*pcsz + rboard.Min.X;
	pos.Y = pos.Y*pcsz + rboard.Min.Y;
	bbr = draw.Rect(0, 0, N*pcsz, N*pcsz);
	bb = image.NewRGBA(bbr.Max.X, bbr.Max.Y);
	bbmask = image.NewRGBA(bbr.Max.X, bbr.Max.Y);	// actually just a bitmap
	bb2r = draw.Rect(0, 0, N*pcsz, N*pcsz+DY);
	bb2 = image.NewRGBA(bb2r.Dx(), bb2r.Dy());
	bb2mask = image.NewRGBA(bb2r.Dx(), bb2r.Dy());	// actually just a bitmap
	draw.Draw(screen, screenr, draw.White, nil, draw.ZP);
	drawboard();
	setpiece(piece);
	if piece != nil {
		drawpiece()
	}
	lastmx = movemouse();
	newscreen = true;
	display.FlushImage();
}

func quitter(c <-chan bool) {
	<-c;
	os.Exit(0);
}

func Play(pp []Piece, ctxt draw.Context) {
	display = ctxt;
	screen = ctxt.Screen();
	screenr = draw.Rect(0, 0, screen.Width(), screen.Height());
	pieces = pp;
	N = len(pieces[0].d);
	initPieces();
	rand.Seed(int64(time.Nanoseconds() % (1e9 - 1)));
	whitemask = draw.White.SetAlpha(0x7F);
	tsleep = 50;
	timerc = time.Tick(int64(tsleep/2) * 1e6);
	suspc = make(chan bool);
	mousec = make(chan draw.Mouse);
	resizec = ctxt.ResizeChan();
	kbdc = make(chan int);
	go quitter(ctxt.QuitChan());
	go suspproc();
	points = 0;
	redraw(false);
	play();
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.