games/galaxy: add n-body simulator

front
spew 2017-02-18 09:08:51 -06:00
parent 412b7501e4
commit 5aabf85d7c
9 changed files with 1507 additions and 0 deletions

231
sys/man/1/galaxy Normal file
View File

@ -0,0 +1,231 @@
.TH GALAXY 1
.SH NAME
galaxy, mkgalaxy \- galactic n-body simulator
.SH SYNOPSIS
.B games/galaxy
[
.I options
] [
.B -i
] [
.I file
]
.br
.B games/mkgalaxy
[
.I options
] [
.B -i
] [
.B -f
.I file
]
.I size
.SH DESCRIPTION
.I Galaxy
is an n-body simulator that uses a Barnes-Hut quad-tree
to calculate gravitational interactions.
Typical usage is to read a galaxy file (see
.IR galaxy (6))
from standard input
using the
.B -i
command-line option or from a
.I file
using the
.B -f
option. If no file is read then the simulator starts with an empty
universe.
.SS Mouse commands
.PP
New planetary bodies can be created with mouse button 1.
Holding button 1 will reposition the body.
Holding a button 1-2 chord changes the mass/size
of the body. Holding a button 1-3 chord
changes the initial velocity of the body. Releasing button 1
restarts the simulator with the new body in motion. When new
bodies are created, the simulator maintains the Galilean (inertial)
reference frame.
.PP
Mouse button 3 opens a menu with the following options:
.TP
.B move
Change the visible region of the simulation
by holding button 1 and dragging. Any other mouse
button restarts the simulation.
.TP
.B zoom
Prompts for a floating point value to change the scale of the
simulation. E.g. a value of 2 will halve the scale (zoom in)
and a value of 0.5 will double the scale (zoom out).
.TP
.B speed
Prompts for a floating point value to change the speed of
the simulation. E.g. a value of 2 will double the speed
of the simulation and a value of 0.5 will
halve the speed. Accuracy is sacrificed for greater speed.
.TP
.B gravity
Prompts for a floating point value to change the gravitational
constant. E.g. a value of 2 will double the force exerted by
gravity and a value of 0.5 will halve it.
.TP
.B save
Prompts for a file name to save the current galaxy as a
.IR galaxy (6)
file.
.TP
.B load
Prompts for a file name to load the galaxy from the
.IR galaxy (6)
file.
.TP
.B exit
Exits the simulator.
.SS Keyboard commands
The following keys are recognized as commands:
.TP
.B a
Show accelerations as vectors.
.TP
.B v
Show velocities as vectors.
.TP
.B s
Show statistics such as the number of bodies being
simulated, the maximum depth of the quad-tree, and the
average number of calculations made per body.
.TP
.B q
Exit the simulator.
.TP
.B space
Pause and unpause the simulator.
.TP
.B del
Exit the simulator.
.SS Command-line options
Certain aspects of the galaxy simulator are controlled by
the following options:
.TP
.BI -t " throttle"
Causes the process that calculates forces to relinquish
the processor for
.I throttle
milliseconds after each calculation.
.TP
.BI -G " gravity"
Sets the gravitational constant to
.I gravity.
The default value is 1.
.TP
.BI -ε " softening"
Sets the
.I softening
factor to prevent gravitational singularities during
collisions or near-collisions. The default value is 500.
.TP
.BI -f " file"
Reads the galaxy file
.I file
(see
.IR galaxy (6)).
.TP
.B -i
Reads a galaxy file from standard input.
.SS Mkgalaxy
.I Mkgalaxy
is a utility to create galaxies for simulation.
Galaxies can be assembled incrementally by reading an
existing galaxy file from standard input with the
.B -i
command-line option or from a
.I file
with the
.B -f
option. Mkgalaxy then writes to standard output a
.IR galaxy (6)
file with a galaxy of the given
.I size
together with the previously read galaxy.
Galaxies generated by mkgalaxy have characteristics
determined by the following options:
.TP
.BI -d " distance"
.I Distance
determines the spacing between bodies.
The default value is 100.
.TP
.BI -s " size"
Bodies have the given
.IR size .
The default value is 25.
.TP
.BI -v " velocity"
Bodies have the given
.I velocity
in a random direction.
The default value is 0.
.TP
.BI -av " angular velocity"
Bodies have the given
.I "angular velocity"
relative to the center of mass of the new galaxy being generated.
The default value is 0.
.TP
.BI -gv " x,y"
The entire galaxy being generated is given the directional velocity determined
by the vector
.RI ( x,y ).
The default value is (0, 0).
.TP
.BI -o " x,y"
The entire galaxy being generated is offset by the vector
.RI ( x,y ).
The default value is (0, 0).
.TP
.B -sq
The galaxy being generated is a square. Without this option, the galaxy
will be circular.
.PP
The arguments to the
.BR -d ,
.BR -s ,
.BR -v ,
and
.B -av
arguments have the form
.B s
or
.B s±r
where s and r are double-precision floating point numbers.
.B S
is the base value and
.B r
if given determines a range in which the value will vary randomly
from the base.
.SH EXAMPLES
Two rotating circles destroy each other:
.IP
.EX
games/mkgalaxy -av 100 -d 60±50 -v 10 2000 |
games/mkgalaxy -i -av -70 -d 80±50 -v 10 -o 6000,2000 -gv -80,40 3000 |
games/galaxy -i
.PP
Cool patterns made by a square galaxy:
.IP
.EX
games/mkgalaxy -sq -av 20 5000 | games/galaxy -i
.SH SOURCE
.B /sys/src/games/galaxy
.SH SEE ALSO
J. Barnes & P. Hut (December 1986). "A hierarchical O(N log N) force-calculation algorithm".
.IR Nature .
324 (4): 446449.
.PP
.IR galaxy (6)
.SH HISTORY
.I Galaxy
and
.I mkgalaxy
first appeared in 9front (Feb, 2017).

41
sys/man/6/galaxy Normal file
View File

@ -0,0 +1,41 @@
.TH GALAXY 6
.SH NAME
galaxy \- representations of n-body simulations
.SH DESCRIPTION
Files of this format are interpreted by
.IR galaxy (1)
as describing the inital condition of n-body simulations
or the saved state of simulation in progress.
A galaxy file is a UTF stream of instruction lines. The
instruction is given by the first space delimited word. The
following instructions are accepted.
.TP
.B MKBODY
The rest of the line must contain 5 white space delimited
double-precision floating point numbers. They represent a
body's x coordinate, y coordinate, x velocity component,
y velocity component, and size respectively.
.TP
.B ORIG
The rest of the line must contain 2 white space delimited
double-precision floating point numbers. They represent
the current location of the origin with respect to the
view window of
.IR galaxy (1).
.TP
.B DT
The rest of the line must contain a double-precision
floating point number which determines the time-scale
of the simulation.
.TP
.B SCALE
The rest of the line must contain a double-precision
floating point number which determines the scale
of the view of the simulation.
.TP
.B GRAV
The rest of the line must contain a double-precision
floating point number which determines the gravitational
constant of the simulation.
.SH SEE ALSO
.IR galaxy (1)

210
sys/src/games/galaxy/body.c Normal file
View File

@ -0,0 +1,210 @@
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <bio.h>
#include "galaxy.h"
void
glxyinit(void)
{
glxy.a = calloc(5, sizeof(Body));
if(glxy.a == nil)
sysfatal("could not allocate glxy: %r");
glxy.l = 0;
glxy.max = 5;
}
Body*
body(void)
{
Body *b;
if(glxy.l == glxy.max) {
glxy.max *= 2;
glxy.a = realloc(glxy.a, sizeof(Body) * glxy.max);
if(glxy.a == nil)
sysfatal("could not realloc glxy: %r");
}
b = glxy.a + glxy.l++;
*b = ZB;
return b;
}
void
drawbody(Body *b)
{
Point pos, v;
int s;
pos.x = b->x / scale + orig.x;
pos.y = b->y / scale + orig.y;
s = b->size/scale;
fillellipse(screen, pos, s, s, b->col, ZP);
v.x = b->v.x/scale*10;
v.y = b->v.y/scale*10;
if(v.x != 0 || v.y != 0)
line(screen, pos, addpt(pos, v), Enddisc, Endarrow, 0, b->col, ZP);
flushimage(display, 1);
}
Vector
center(void)
{
Body *b;
Vector gc, gcv;
double mass;
if(glxy.l == 0)
return (Vector){0, 0};
gc.x = gc.y = gcv.x = gcv.y = mass = 0;
for(b = glxy.a; b < glxy.a+glxy.l; b++) {
gc.x += b->x * b->mass;
gc.y += b->y * b->mass;
gcv.x += b->v.x * b->mass;
gcv.y += b->v.y * b->mass;
mass += b->mass;
}
gc.x /= mass;
gc.y /= mass;
gcv.x /= mass;
gcv.y /= mass;
for(b = glxy.a; b < glxy.a+glxy.l; b++) {
b->x -= gc.x;
b->y -= gc.y;
b->v.x -= gcv.x;
b->v.y -= gcv.y;
}
return gc;
}
int
Bfmt(Fmt *f)
{
Body *b;
int r;
b = va_arg(f->args, Body*);
r = fmtprint(f, "MKBODY %g %g ", b->x, b->y);
if(r < 0)
return -1;
r = fmtprint(f, "%g %g ", b->v.x, b->v.y);
if(r < 0)
return -1;
return fmtprint(f, "%g", b->size);
}
enum {
MKBODY,
ORIG,
DT,
SCALE,
GRAV,
NOCMD,
};
int
getcmd(char *l)
{
static char *cmds[] = {
[MKBODY] "MKBODY",
[ORIG] "ORIG",
[DT] "DT",
[SCALE] "SCALE",
[GRAV] "GRAV",
};
int cmd;
for(cmd = 0; cmd < nelem(cmds); cmd++) {
if(strcmp(l, cmds[cmd]) == 0)
return cmd;
}
sysfatal("getcmd: no such command %s", l);
return NOCMD;
}
void
readglxy(int fd)
{
static Biobuf bin;
char *line;
double f;
int cmd, len;
Body *b;
glxy.l = 0;
Binit(&bin, fd, OREAD);
for(;;) {
line = Brdline(&bin, ' ');
len = Blinelen(&bin);
if(line == nil) {
if(len == 0)
break;
sysfatal("load: malformed command");
}
line[len-1] = '\0';
cmd = getcmd(line);
line = Brdline(&bin, '\n');
if(line == nil) {
if(len == 0)
sysfatal("load: malformed command");
sysfatal("load: read error: %r");
}
len = Blinelen(&bin);
line[len-1] = '\0';
switch(cmd) {
case MKBODY:
b = body();
b->x = strtod(line, &line);
b->y = strtod(line, &line);
b->v.x = strtod(line, &line);
b->v.y = strtod(line, &line);
b->size = strtod(line, nil);
b->mass = b->size*b->size*b->size;
b->col = randcol();
CHECKLIM(b, f);
break;
case ORIG:
orig.x = strtol(line, &line, 10);
orig.y = strtol(line, nil, 10);
break;
case DT:
dt = strtod(line, nil);
dt² = dt*dt;
break;
case SCALE:
scale = strtod(line, nil);
break;
case GRAV:
G = strtod(line, nil);
break;
}
}
Bterm(&bin);
}
void
writeglxy(int fd)
{
static Biobuf bout;
Body *b;
Binit(&bout, fd, OWRITE);
Bprint(&bout, "ORIG %d %d\n", orig.x, orig.y);
Bprint(&bout, "SCALE %g\n", scale);
Bprint(&bout, "DT %g\n", dt);
Bprint(&bout, "GRAV %g\n", G);
for(b = glxy.a; b < glxy.a + glxy.l; b++)
Bprint(&bout, "%B\n", b);
Bterm(&bout);
}

View File

@ -0,0 +1,616 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <draw.h>
#include <thread.h>
#include <mouse.h>
#include <cursor.h>
#include <keyboard.h>
#include "galaxy.h"
Cursor crosscursor = {
{-7, -7},
{0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
0x03, 0xC0, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x03, 0xC0,
0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, },
{0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE,
0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00, }
};
Cursor pausecursor={
0, 0,
0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x07, 0xe0,
0x07, 0xe0, 0x07, 0xe0, 0x03, 0xc0, 0x0F, 0xF0,
0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8,
0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x3F, 0xFC,
0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x04, 0x20,
0x04, 0x20, 0x06, 0x60, 0x02, 0x40, 0x0C, 0x30,
0x10, 0x08, 0x14, 0x08, 0x14, 0x28, 0x12, 0x28,
0x0A, 0x50, 0x16, 0x68, 0x20, 0x04, 0x3F, 0xFC,
};
enum {
STK = 8192,
MOVE = 0,
ZOOM,
SPEED,
GRAV,
SAVE,
LOAD,
EXIT,
MEND,
};
Cursor *cursor;
Mousectl *mc;
Keyboardctl kc;
double
G = 1,
θ = 1,
scale = 30,
ε = 500,
dt = .2,
LIM = 10,
dt²;
char *file;
int showv, showa, throttle, paused;
char *menustr[] = {
[SAVE] "save",
[LOAD] "load",
[ZOOM] "zoom",
[SPEED] "speed",
[GRAV] "gravity",
[MOVE] "move",
[EXIT] "exit",
[MEND] nil
};
Menu menu = {
.item menustr
};
Image*
randcol(void)
{
static struct {
ulong c;
Image *i;
} cols[] = {
DWhite, nil,
DRed, nil,
DGreen, nil,
DCyan, nil,
DMagenta, nil,
DYellow, nil,
DPaleyellow, nil,
DDarkyellow, nil,
DDarkgreen, nil,
DPalegreen, nil,
DPalebluegreen, nil,
DPaleblue, nil,
DPalegreygreen, nil,
DYellowgreen, nil,
DGreyblue, nil,
DPalegreyblue, nil,
};
int r;
r = nrand(nelem(cols));
if(cols[r].i == nil)
cols[r].i = allocimage(display, Rect(0,0,1,1), screen->chan, 1, cols[r].c);
return cols[r].i;
}
void
pause(int p, int pri)
{
static int paused, ppri;
switch(p) {
default:
sysfatal("invalid pause value %d:", p);
break;
case 0:
if(pri > ppri)
ppri = pri;
if(paused)
break;
paused = 1;
qlock(&glxy);
break;
case 1:
if(!paused || pri < ppri)
break;
paused = ppri = 0;
qunlock(&glxy);
break;
}
}
void
drawstats(void)
{
Point p;
static char buf[1024];
snprint(buf, sizeof(buf), "Number of bodies: %d", glxy.l);
p = addpt(screen->r.min, (Point){5, 3});
string(screen, p, display->white, ZP, font, buf);
snprint(buf, sizeof(buf), "Avg. calculations per body: %g", avgcalcs);
p = addpt(p, (Point){0, font->height});
string(screen, p, display->white, ZP, font, buf);
snprint(buf, sizeof(buf), "Max depth of quad tree: %d", quaddepth);
p = addpt(p, (Point){0, font->height});
string(screen, p, display->white, ZP, font, buf);
}
void
drawglxy(void)
{
Point pos, va;
Body *b;
int s;
draw(screen, screen->r, display->black, 0, ZP);
for(b = glxy.a; b < glxy.a + glxy.l; b++) {
pos.x = b->x / scale + orig.x;
pos.y = b->y / scale + orig.y;
s = b->size/scale;
fillellipse(screen, pos, s, s, b->col, ZP);
if(showv) {
va.x = b->v.x/scale;
va.y = b->v.y/scale;
if(va.x != 0 || va.y != 0)
line(screen, pos, addpt(pos, va), Enddisc, Endarrow, 0, b->col, ZP);
}
if(showa) {
va.x = b->a.x/scale*50;
va.y = b->a.y/scale*50;
if(va.x != 0 || va.y != 0)
line(screen, pos, addpt(pos, va), Enddisc, Endarrow, 0, b->col, ZP);
}
}
STATS(drawstats();)
flushimage(display, 1);
}
void
setsize(Body *b)
{
Point pos, d;
double h;
pos.x = b->x / scale + orig.x;
pos.y = b->y / scale + orig.y;
d = subpt(mc->xy, pos);
h = hypot(d.x, d.y);
b->size = h == 0 ? scale : h*scale;
b->mass = b->size*b->size*b->size;
}
void
setvel(Body *b)
{
Point pos, d;
pos.x = b->x / scale + orig.x;
pos.y = b->y / scale + orig.y;
d = subpt(mc->xy, pos);
b->v.x = (double)d.x*scale/10;
b->v.y = (double)d.y*scale/10;
}
void
setpos(Body *b)
{
b->x = (mc->xy.x - orig.x) * scale;
b->y = (mc->xy.y - orig.y) * scale;
}
void
dosize(Body *b)
{
Point p;
p = mc->xy;
for(;;) {
setsize(b);
drawglxy();
drawbody(b);
readmouse(mc);
if(mc->buttons != 3)
break;
}
moveto(mc, p);
}
void
dovel(Body *b)
{
Point p;
p = mc->xy;
for(;;) {
setvel(b);
drawglxy();
drawbody(b);
readmouse(mc);
if(mc->buttons != 5)
break;
}
moveto(mc, p);
}
void
dobody(void)
{
Vector gc;
double f;
Body *b;
pause(0, 0);
b = body();
setpos(b);
setvel(b);
setsize(b);
b->col = randcol();
for(;;) {
drawglxy();
drawbody(b);
readmouse(mc);
if(!(mc->buttons & 1))
break;
if(mc->buttons == 3)
dosize(b);
else if(mc->buttons == 5)
dovel(b);
else
setpos(b);
}
CHECKLIM(b, f);
gc = center();
orig.x += gc.x / scale;
orig.y += gc.y / scale;
pause(1, 0);
}
char*
getinput(char *info, char *sug)
{
static char buf[1024];
static Channel *rchan;
char *input;
int r;
if(rchan == nil)
rchan = chancreate(sizeof(Rune), 20);
if(sug != nil)
strecpy(buf, buf+1024, sug);
else
buf[0] = '\0';
kc.c = rchan;
r = enter(info, buf, sizeof(buf), mc, &kc, nil);
kc.c = nil;
if(r < 0)
sysfatal("save: could not get filename: %r");
input = strdup(buf);
if(input == nil)
sysfatal("getinput: could not save input: %r");
return input;
}
void
move(void)
{
Point od;
setcursor(mc, &crosscursor);
for(;;) {
for(;;) {
readmouse(mc);
if(mc->buttons & 1)
break;
if(mc->buttons & 4) {
setcursor(mc, cursor);
return;
}
}
od = subpt(orig, mc->xy);
for(;;) {
readmouse(mc);
if(!(mc->buttons & 1))
break;
orig = addpt(od, mc->xy);
drawglxy();
}
}
}
void
load(int fd)
{
orig = divpt(subpt(screen->r.max, screen->r.min), 2);
orig = addpt(orig, screen->r.min);
readglxy(fd);
center();
}
void
domenu(void)
{
int fd;
char *s;
double z;
pause(0, 0);
switch(menuhit(3, mc, &menu, nil)) {
case SAVE:
s = getinput("Enter file:", file);
if(s == nil || *s == '\0')
break;
free(file);
file = s;
fd = create(file, OWRITE, 0666);
if(fd < 0)
sysfatal("domenu: could not create file %s: %r", file);
writeglxy(fd);
close(fd);
break;
case LOAD:
s = getinput("Enter file:", file);
if(s == nil || *s == '\0')
break;
free(file);
file = s;
fd = open(file, OREAD);
if(fd < 0)
sysfatal("domenu: could not open file %s: %r", file);
load(fd);
close(fd);
break;
case ZOOM:
s = getinput("Zoom multiplier:", nil);
if(s == nil || *s == '\0')
break;
z = strtod(s, nil);
free(s);
if(z <= 0)
break;
scale /= z;
break;
case SPEED:
s = getinput("Speed multiplier:", nil);
if(s == nil || *s == '\0')
break;
z = strtod(s, nil);
free(s);
if(z <= 0)
break;
dt *= z;
dt² = dt*dt;
break;
case GRAV:
s = getinput("Gravity multiplier:", nil);
if(s == nil || *s == '\0')
break;
z = strtod(s, nil);
free(s);
if(z <= 0)
break;
G *= z;
break;
case MOVE:
move();
break;
case EXIT:
threadexitsall(nil);
break;
}
drawglxy();
pause(1, 0);
}
void
mousethread(void*)
{
threadsetname("mouse");
for(;;) {
readmouse(mc);
switch(mc->buttons) {
case 1:
dobody();
break;
case 4:
domenu();
break;
}
}
}
void
resizethread(void*)
{
threadsetname("resize");
for(;;) {
recv(mc->resizec, nil);
pause(0, 0);
if(getwindow(display, Refnone) < 0)
sysfatal("resize failed: %r");
drawglxy();
pause(1, 0);
}
}
void
kbdthread(void*)
{
Keyboardctl *realkc;
Rune r;
threadsetname("keyboard");
if(realkc = initkeyboard(nil), realkc == nil)
sysfatal("kbdthread: could not initkeyboard: %r");
for(;;) {
recv(realkc->c, &r);
if(r == Kdel) {
closedisplay(display);
threadexitsall(nil);
}
if(kc.c != nil)
send(kc.c, &r);
else switch(r) {
case 'q':
closedisplay(display);
threadexitsall(nil);
break;
case 's':
stats ^= 1;
break;
case 'v':
showv ^= 1;
break;
case 'a':
showa ^= 1;
break;
case ' ':
paused ^= 1;
if(paused) {
cursor = &pausecursor;
pause(0, 1);
} else {
cursor = nil;
pause(1, 1);
}
setcursor(mc, cursor);
}
}
}
/* verlet barnes-hut */
void
simulate(void*)
{
Body *b;
double f;
threadsetname("simulate");
for(;;) {
qlock(&glxy);
if(throttle)
sleep(throttle);
drawglxy();
Again:
space.t = EMPTY;
quads.l = 0;
STATS(quaddepth = 0;)
for(b = glxy.a; b < glxy.a + glxy.l; b++) {
if(quadins(b, LIM) == -1) {
growquads();
goto Again;
}
}
STATS(avgcalcs = 0;)
for(b = glxy.a; b < glxy.a + glxy.l; b++) {
b->a.x = b->newa.x;
b->a.y = b->newa.y;
b->newa.x = b->newa.y = 0;
STATS(calcs = 0;)
quadcalc(space, b, LIM);
STATS(avgcalcs += calcs;)
}
STATS(avgcalcs /= glxy.l;)
for(b = glxy.a; b < glxy.a + glxy.l; b++) {
b->x += dt*b->v.x + dt²*b->a.x/2;
b->y += dt*b->v.y + dt²*b->a.y/2;
b->v.x += dt*(b->a.x + b->newa.x)/2;
b->v.y += dt*(b->a.y + b->newa.y)/2;
CHECKLIM(b, f);
}
qunlock(&glxy);
}
}
void
usage(void)
{
fprint(2, "Usage: %s [-t throttle] [-G gravity] [-ε smooth] [-i] [file]\n", argv0);
threadexitsall("usage");
}
void
threadmain(int argc, char **argv)
{
int doload;
doload = 0;
ARGBEGIN {
default:
usage();
break;
case 't':
throttle = strtol(EARGF(usage()), nil, 0);
break;
case 'G':
G = strtod(EARGF(usage()), nil);
break;
case L'ε':
ε = strtod(EARGF(usage()), nil);
break;
case 'i':
doload++;
break;
} ARGEND
if(argc > 1)
usage();
fmtinstall('B', Bfmt);
if(argc == 1) {
if(doload++)
usage();
file = strdup(argv[0]);
if(file == nil)
sysfatal("threadmain: could not save file name: %r");
close(0);
if(open(file, OREAD) != 0)
sysfatal("threadmain: could not open file: %r");
}
if(initdraw(nil, nil, "Galaxy") < 0)
sysfatal("initdraw failed: %r");
if(mc = initmouse(nil, screen), mc == nil)
sysfatal("initmouse failed: %r");
dt² = dt*dt;
orig = divpt(subpt(screen->r.max, screen->r.min), 2);
orig = addpt(orig, screen->r.min);
glxyinit();
quadsinit();
if(doload)
load(0);
close(0);
threadcreate(mousethread, nil, STK);
threadcreate(resizethread, nil, STK);
threadcreate(kbdthread, nil, STK);
proccreate(simulate, nil, STK);
threadexits(nil);
}

View File

@ -0,0 +1,83 @@
typedef struct QB QB;
typedef struct Body Body;
typedef struct Quad Quad;
typedef struct Vector Vector;
struct QB {
union {
Quad *q;
Body *b;
};
int t;
};
struct Vector {
double x, y;
};
struct Body {
Vector;
Vector v, a, newa;
double size, mass;
Image *col;
};
struct Quad {
Vector;
QB c[4];
double mass;
};
#pragma varargck type "B" Body*
struct {
QLock;
Body *a;
int l, max;
} glxy;
struct {
Quad *a;
int l, max;
} quads;
#define π2 6.28318530718e0
enum {
EMPTY,
QUAD,
BODY,
};
Point orig;
double G, θ, scale, ε, LIM, dt, dt²;
Body ZB;
QB space;
Image *randcol(void);
Body *body(void);
void drawbody(Body*);
Vector center(void);
void glxyinit(void);
void readglxy(int);
void writeglxy(int);
int Bfmt(Fmt*);
void quadcalc(QB, Body*, double);
int quadins(Body*, double);
void growquads(void);
void quadsinit(void);
int stats;
int quaddepth;
int calcs;
double avgcalcs;
#define STATS(e) if(stats) {e}
#define CHECKLIM(b, f) \
if(((f) = fabs((b)->x)) > LIM/2) \
LIM = (f)*2; \
if(((f) = fabs((b)->y)) > LIM/2) \
LIM = (f)*2

View File

@ -0,0 +1,14 @@
</$objtype/mkfile
TARG=galaxy mkgalaxy
BIN=/$objtype/bin/games
OGALAXY=galaxy.$O quad.$O body.$O
OMKGALAXY=mkgalaxy.$O body.$O
</sys/src/cmd/mkmany
$O.galaxy: $OGALAXY
$LD $LDFLAGS -o $target $prereq
$O.mkgalaxy: $OMKGALAXY
$LD $LDFLAGS -o $target $prereq

View File

@ -0,0 +1,171 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <draw.h>
#include "galaxy.h"
Vector o, gv;
double
d = 100, drand,
sz = 25, szrand,
v, vrand,
av, avrand;
int new, c = 1;
void quadcalc(QB, Body*, double){}
Image *randcol(void){ return nil; }
void
usage(void)
{
fprint(2, "Usage: %s [-d dist[±r]]\n\t[-s size[±r]] [-v vel[±r]]\n\t[-av angvel[±r]] [-gv xdir,ydir]\n\t[-o xoff,yoff] [-f file]\n\t[-sq] [-i] size\n", argv0);
exits("usage");
}
Vector
polar(double ang, double mag)
{
Vector v;
v.x = cos(ang)*mag;
v.y = sin(ang)*mag;
return v;
}
Vector
getvec(char *str)
{
Vector v;
v.x = strtod(str, &str);
if(*str != ',')
usage();
v.y = strtod(str+1, nil);
return v;
}
double
getvals(char *str, double *rand)
{
Rune r;
double val;
int i;
val = strtod(str, &str);
i = chartorune(&r, str);
if(r == L'±')
*rand = strtod(str+i, nil);
else
*rand = 0;
return val;
}
#define RAND(r) ((r)*(frand()*2 - 1))
void
mkbodies(double lim)
{
Body *b;
Vector p;
double x, y;
for(x = -lim/2; x < lim/2; x += d)
for(y = -lim/2; y < lim/2; y += d) {
p.x = x + RAND(drand);
p.y = y + RAND(drand);
if(c)
if(hypot(p.x, p.y) > lim/2)
continue;
b = body();
b->Vector = p;
b->v = polar(frand()*π2, v+RAND(vrand));
b->v.x += gv.x - p.y*(av + RAND(avrand))/1000;
b->v.y += gv.y + p.x*(av + RAND(avrand))/1000;
b->size = sz + RAND(szrand);
}
}
void
main(int argc, char **argv)
{
static Biobuf bout;
Body *b;
double lim;
int fd;
char *a;
srand(truerand());
fmtinstall('B', Bfmt);
glxyinit();
ARGBEGIN {
case 'f':
fd = open(EARGF(usage()), OREAD);
if(fd < 0)
sysfatal("Could not open file %s: %r", *argv);
readglxy(fd);
close(fd);
break;
case 'i':
readglxy(0);
break;
case 's':
a = EARGF(usage());
switch(a[0]) {
case 'q':
if(a[1] != '\0')
usage();
c = 0;
break;
default:
sz = getvals(a, &szrand);
break;
}
break;
case 'a':
a = EARGF(usage());
if(a[0] != 'v' || a[1] != '\0')
usage();
argc--;
argv++;
av = getvals(*argv, &avrand);
break;
case 'g':
a = EARGF(usage());
if(a[0] != 'v' || a[1] != '\0')
usage();
argc--;
argv++;
gv = getvec(*argv);
break;
case 'v':
v = getvals(EARGF(usage()), &vrand);
break;
case 'o':
o = getvec(EARGF(usage()));
break;
case 'd':
d = getvals(EARGF(usage()), &drand);
break;
} ARGEND
if(argc != 1)
usage();
new = glxy.l;
lim = strtod(*argv, nil);
mkbodies(lim);
Binit(&bout, 1, OWRITE);
for(b = glxy.a; b < glxy.a + new; b++)
Bprint(&bout, "%B\n", b);
for(b = glxy.a+new; b < glxy.a+glxy.l; b++) {
b->x += o.x;
b->y += o.y;
Bprint(&bout, "%B\n", b);
}
Bterm(&bout);
exits(nil);
}

140
sys/src/games/galaxy/quad.c Normal file
View File

@ -0,0 +1,140 @@
#include <u.h>
#include <libc.h>
#include <draw.h>
#include "galaxy.h"
void
growquads(void)
{
quads.max *= 2;
quads.a = realloc(quads.a, sizeof(Quad) * quads.max);
if(quads.a == nil)
sysfatal("could not realloc quads: %r");
}
Quad*
quad(Body *b)
{
Quad *q;
if(quads.l == quads.max)
return nil;
q = quads.a + quads.l++;
memset(q->c, 0, sizeof(QB)*4);
q->x = b->x;
q->y = b->y;
q->mass = b->mass;
return q;
}
int
quadins(Body *nb, double size)
{
QB *qb;
Quad *q;
Body *b;
double mass, qx, qy;
uint qxy;
int d;
if(space.t == EMPTY) {
space.t = BODY;
space.b = nb;
return 0;
}
d = 0;
qb = &space;
qx = 0.0;
qy = 0.0;
for(;;) {
if(qb->t == BODY) {
b = qb->b;
qxy = b->x < qx ? 0 : 1;
qxy |= b->y < qy ? 0 : 2;
qb->t = QUAD;
if((qb->q = quad(b)) == nil)
return -1;
qb->q->c[qxy].t = BODY;
qb->q->c[qxy].b = b;
}
q = qb->q;
mass = q->mass + nb->mass;
q->x = (q->x*q->mass + nb->x*nb->mass) / mass;
q->y = (q->y*q->mass + nb->y*nb->mass) / mass;
q->mass = mass;
qxy = nb->x < qx ? 0 : 1;
qxy |= nb->y < qy ? 0 : 2;
if(q->c[qxy].t == EMPTY) {
q->c[qxy].t = BODY;
q->c[qxy].b = nb;
STATS(if(d > quaddepth) quaddepth = d;)
return 0;
}
STATS(d++;)
qb = &q->c[qxy];
size /= 2;
qx += qxy&1 ? size/2 : -size/2;
qy += qxy&2 ? size/2 : -size/2;
}
}
void
quadcalc(QB qb, Body *b, double size)
{
double fx÷mm, fy÷mm, dx, dy, h, G÷h³;
for(;;) switch(qb.t) {
default:
sysfatal("quadcalc: No such type");
case EMPTY:
return;
case BODY:
if(qb.b == b)
return;
dx = qb.b->x - b->x;
dy = qb.b->y - b->y;
h = hypot(hypot(dx, dy), ε);
G÷h³ = G / (h*h*h);
fx÷mm = dx * G÷h³;
fy÷mm = dy * G÷h³;
b->newa.x += fx÷mm * qb.b->mass;
b->newa.y += fy÷mm * qb.b->mass;
STATS(calcs++;)
return;
case QUAD:
dx = qb.q->x - b->x;
dy = qb.q->y - b->y;
h = hypot(dx, dy);
if(h != 0.0 && size/h < θ) {
h = hypot(h, ε);
G÷h³ = G / (h*h*h);
fx÷mm = dx * G÷h³;
fy÷mm = dy * G÷h³;
b->newa.x += fx÷mm * qb.q->mass;
b->newa.y += fy÷mm * qb.q->mass;
STATS(calcs++;)
return;
}
size /= 2;
quadcalc(qb.q->c[0], b, size);
quadcalc(qb.q->c[1], b, size);
quadcalc(qb.q->c[2], b, size);
qb = qb.q->c[3];
break; /* quadcalc(q->q[3], b, size); */
}
}
void
quadsinit(void)
{
quads.a = calloc(5, sizeof(Body));
if(quads.a == nil)
sysfatal("could not allocate quads: %r");
quads.l = 0;
quads.max = 5;
}

View File

@ -25,6 +25,7 @@ DIRS=\
blabs\
c64\
doom\
galaxy\
gb\
gba\
mahjongg\