Last active
September 27, 2017 01:29
-
-
Save SamuelDavis/fefaf23be86c98a6ddd47dcc3df5bebc to your computer and use it in GitHub Desktop.
Will Thimbleby's tiny roguelike (stealthy version): http://will.thimbleby.net/roguelike/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.swing.*; | |
import java.applet.Applet; | |
import java.awt.*; | |
import java.awt.event.KeyEvent; | |
import java.awt.event.KeyListener; | |
import java.awt.event.MouseAdapter; | |
import java.awt.event.MouseEvent; | |
import java.awt.font.FontRenderContext; | |
import java.awt.font.TextLayout; | |
import java.awt.geom.Rectangle2D; | |
import java.util.HashMap; | |
import java.util.Vector; | |
public class StealthyRogueApplet extends Applet { | |
static Font drawFont = new Font("Monospaced", Font.PLAIN, 14); | |
static int numDirections = 8; | |
static int directions[][] = {{-1, -1}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {0, 0}}; | |
static int facings[][] = {{0, 7, 6}, {1, 8, 5}, {2, 3, 4}}; | |
static float unitDirections[][]; | |
static { | |
unitDirections = new float[8][2]; | |
for (int i = 0; i < 8; i++) { | |
float d = (float) Math.sqrt(directions[i][0] * directions[i][0] + directions[i][1] * directions[i][1]); | |
unitDirections[i][0] = (float) directions[i][0] / d; | |
unitDirections[i][1] = (float) directions[i][1] / d; | |
} | |
} | |
DrawPanel drawPanel; | |
int width = 50; | |
int height = 50; | |
char map[][] = new char[50][50]; | |
char floorchar = '\u00B7'; | |
Vector<StealthyRogueApplet.Monster> monsters = new Vector<StealthyRogueApplet.Monster>(); | |
Character player; | |
Color whiteColor = new Color(255, 255, 255, 255); | |
float monsterSees[][] = new float[50][50]; | |
int level = 0; | |
Monster mtypes[] = { | |
new Monster(3, 4, 'a', "ant"), | |
new Monster(5, 6, 'd', "dog"), | |
new Monster(7, 8, 'k', "kobol"), | |
new Monster(3, 12, 'g', "goblin"), | |
new Monster(10, 6, 'h', "hobbit"), | |
new Monster(7, 14, 'x', "xan"), | |
new Monster(15, 30, 'D', "dragon")}; | |
public void tick() { | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) | |
monsterSees[x][y] = 0; | |
} | |
player.see(); | |
for (int i = 0; i < monsters.size(); i++) { | |
Monster m = monsters.elementAt(i); | |
m.ai(); | |
if (player.sees[m.x][m.y]) | |
m.see(); | |
} | |
} | |
public void genMap() { | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) | |
map[x][y] = '#'; | |
} | |
Vector<StealthyRogueApplet.Room> connectedRooms = new Vector<StealthyRogueApplet.Room>(); | |
Vector<StealthyRogueApplet.Room> rooms = new Vector<StealthyRogueApplet.Room>(); | |
int nrooms = (int) (Math.random() * 4) + 5; | |
for (int i = 0; i < nrooms; i++) { | |
rooms.add(new Room()); | |
} | |
connectedRooms.add(new Room()); | |
while (rooms.size() > 0) { | |
int a, b; | |
a = (int) (Math.random() * connectedRooms.size()); | |
b = (int) (Math.random() * rooms.size()); | |
connectedRooms.get(a).createPathTo(rooms.get(b)); | |
connectedRooms.add(rooms.get(b)); | |
rooms.remove(b); | |
} | |
// gold | |
for (int i = 0; i < (level + 10); i++) { | |
int x = (int) (Math.random() * width); | |
int y = (int) (Math.random() * height); | |
map[x][y] = '$'; | |
} | |
// potions | |
for (int i = 0; i < 10; i++) { | |
int x = (int) (Math.random() * width); | |
int y = (int) (Math.random() * height); | |
map[x][y] = '!'; | |
} | |
// down staircase | |
while (true) { | |
int x = (int) (Math.random() * width); | |
int y = (int) (Math.random() * height); | |
if (map[x][y] == floorchar) { | |
map[x][y] = '>'; | |
break; | |
} | |
} | |
// monsters | |
int c = nrooms + (int) (Math.random() * (nrooms + ((float) level / 4))); | |
for (int i = 0; i < c; i++) { | |
int ind = (int) (Math.abs((Math.random() + Math.random() - 1)) * (level + 2)); | |
if (ind >= mtypes.length) ind = mtypes.length - 1; | |
Monster t = mtypes[ind]; | |
t = new Monster(t.health, t.strength, t.symbol, t.name); | |
t.ai_interest = Math.random() / (ind + 2); | |
monsters.add(t); | |
t.place(); | |
} | |
level++; | |
} | |
public void init() { | |
setLayout(new BorderLayout()); | |
drawPanel = new DrawPanel(); | |
drawPanel.setPreferredSize(new Dimension(300, 300)); | |
add(drawPanel, BorderLayout.CENTER); | |
genMap(); | |
player = new Character(); | |
monsters.add(player); | |
player.reset(); | |
drawPanel.repaint(); | |
} | |
public class DrawPanel extends JPanel implements KeyListener { | |
HashMap<java.lang.Character, TextLayout> textcache = new HashMap<java.lang.Character, TextLayout>(); | |
public DrawPanel() { | |
addKeyListener(this); | |
addMouseListener(new MouseAdapter() { | |
public void mousePressed(MouseEvent me) { | |
DrawPanel.this.requestFocusInWindow(); | |
} | |
}); | |
} | |
public boolean isFocusable() { | |
return true; | |
} | |
public void paintComponent(Graphics g) { | |
Graphics2D g2d = (Graphics2D) g; | |
Rectangle r = getBounds(); | |
Rectangle2D rbounds = new Rectangle2D.Float(0f, 0f, (float) r.getWidth() - 1, (float) r.getHeight() - 1); | |
g2d.setColor(Color.black); | |
g2d.fill(rbounds); | |
FontRenderContext frc = g2d.getFontRenderContext(); | |
if (player.health < 0) { | |
g.setColor(new Color(255, 0, 0, 255)); | |
String txt = "Dead & buried on level " + level + " with $" + player.score; | |
TextLayout layout = new TextLayout(txt, drawFont, frc); | |
layout.draw(g2d, (600 / 11 - txt.length()) * 11 / 2, (height + 1) * 11 / 2); | |
return; | |
} | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) { | |
Color tileColor; | |
char c = map[x][y]; | |
if (player.seen[x][y] == false) | |
c = ' '; | |
// lighting | |
float light = 1; | |
if (player.sees[x][y]) { | |
int dx = player.x - x; | |
int dy = player.y - y; | |
float rr = (dx * dx + dy * dy); | |
light = rr > 1 ? 0.5f + 4 / (rr - 1) : 1; | |
if (light > 1) light = 1; | |
} else | |
light = 0.25f; | |
// colouring | |
if (c == '$') | |
tileColor = new Color(1.0f, 1.0f, 0, 1.0f); | |
else if (c == ',' || c == ';' || c == '\'' || c == '`') | |
tileColor = new Color(1.0f, 0, 0, 1.0f); | |
else if (c == '!') | |
tileColor = new Color(0, 1.0f, 0, 1.0f); | |
else | |
tileColor = new Color(1.0f, 1.0f, 1.0f, 1.0f); | |
if (player.sees[x][y]) { | |
for (Monster m : monsters) { | |
if (m.x == x && m.y == y) { | |
tileColor = new Color(1.0f, 0, 0, 1.0f); | |
c = m.symbol; | |
if (c == '@') | |
tileColor = new Color(0, 0, 1.0f, 1.0f); | |
break; | |
} | |
} | |
} else if (player.lineOfSight(x, y)) { | |
for (Monster m : monsters) { | |
if (m.x == x && m.y == y) { | |
tileColor = new Color(1.0f, 0, 0, 1.0f); | |
c = '?'; | |
light = 1.0f; | |
break; | |
} | |
} | |
} | |
if (c == ' ') continue; | |
tileColor = new Color(tileColor.getRed() / 255 * light, tileColor.getGreen() / 255 * light, tileColor.getBlue() / 255 * light, tileColor.getAlpha() / 255); | |
if (monsterSees[x][y] > 0) { | |
g.setColor(new Color(monsterSees[x][y], 0, 0, 0.3f)); | |
g2d.fill(new Rectangle(x * 11, (y - 1) * 11, 11, 11)); | |
} | |
// drawing | |
g.setColor(tileColor); | |
TextLayout layout = (TextLayout) textcache.get(c); | |
if (layout == null) | |
layout = new TextLayout("" + c, drawFont, frc); | |
textcache.put(c, layout); | |
layout.draw(g2d, x * 11, y * 11); | |
} | |
} | |
// draw player info | |
g.setColor(whiteColor); | |
TextLayout layout = new TextLayout("Score: " + player.score, drawFont, frc); | |
layout.draw(g2d, 1 * 11, (height + 1) * 11); | |
layout = new TextLayout("Lvl: " + level, drawFont, frc); | |
layout.draw(g2d, 1 * 11, (height + 2) * 11); | |
layout = new TextLayout("Health: " + player.health + "/" + player.maxhealth, drawFont, frc); | |
layout.draw(g2d, 16 * 11, (height + 1) * 11); | |
layout = new TextLayout("Expr: " + (1 + player.experience / 20) + ":" + player.experience, drawFont, frc); | |
layout.draw(g2d, 16 * 11, (height + 2) * 11); | |
String eBar = player.running ? "Running: " : "Walking: "; | |
for (int e = 0; e < player.energy; e++) | |
eBar = eBar + "*"; | |
layout = new TextLayout(eBar, drawFont, frc); | |
layout.draw(g2d, 16 * 11, (height + 3) * 11); | |
// status | |
for (int i = 0; i < 3; i++) { | |
g.setColor(player.statusColor[i]); | |
layout = new TextLayout(player.status[i], drawFont, frc); | |
layout.draw(g2d, 30 * 11, (height + i + 1) * 11); | |
} | |
} | |
public void keyPressed(KeyEvent e) { | |
switch (e.getKeyCode()) { | |
case KeyEvent.VK_Y: | |
case KeyEvent.VK_NUMPAD7: | |
player.move(0); | |
break; | |
case KeyEvent.VK_K: | |
case KeyEvent.VK_NUMPAD8: | |
case KeyEvent.VK_UP: | |
player.move(1); | |
break; | |
case KeyEvent.VK_U: | |
case KeyEvent.VK_NUMPAD9: | |
player.move(2); | |
break; | |
case KeyEvent.VK_L: | |
case KeyEvent.VK_NUMPAD6: | |
case KeyEvent.VK_RIGHT: | |
player.move(3); | |
break; | |
case KeyEvent.VK_N: | |
case KeyEvent.VK_NUMPAD3: | |
player.move(4); | |
break; | |
case KeyEvent.VK_J: | |
case KeyEvent.VK_NUMPAD2: | |
case KeyEvent.VK_DOWN: | |
player.move(5); | |
break; | |
case KeyEvent.VK_B: | |
case KeyEvent.VK_NUMPAD1: | |
player.move(6); | |
break; | |
case KeyEvent.VK_H: | |
case KeyEvent.VK_NUMPAD4: | |
case KeyEvent.VK_LEFT: | |
player.move(7); | |
break; | |
case KeyEvent.VK_PERIOD: | |
case KeyEvent.VK_NUMPAD5: | |
player.move(8); | |
break; | |
case KeyEvent.VK_SPACE: | |
player.running = !player.running; | |
break; | |
} | |
if (e.getKeyChar() == '>' && map[player.x][player.y] == '>') | |
player.goDown(); | |
repaint(); | |
} | |
public void keyTyped(KeyEvent e) { | |
} | |
public void keyReleased(KeyEvent e) { | |
} | |
} | |
class Character extends Monster { | |
boolean seen[][] = new boolean[50][50]; | |
boolean sees[][] = new boolean[50][50]; | |
int score = 0; | |
int rest = 0; | |
int energy, maxEnergy = 6; | |
boolean running = false; | |
String status[] = {" ", " ", " "}; | |
Color statusColor[] = {whiteColor, whiteColor, whiteColor}; | |
public Character() { | |
super(20, 5, '@', "player"); | |
fov = 60; | |
seeRadius = 20; | |
energy = 6; | |
} | |
public void reset() { | |
super.place(); | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) | |
seen[x][y] = false; | |
} | |
see(); | |
} | |
public void setStatus(String s, Color c) { | |
for (int i = 1; i >= 0; i--) { | |
statusColor[i + 1] = statusColor[i]; | |
status[i + 1] = status[i]; | |
} | |
status[0] = s; | |
statusColor[0] = c; | |
} | |
public void see() { | |
for (int y = 0; y < height; y++) { | |
for (int x = 0; x < width; x++) | |
sees[x][y] = false; | |
} | |
for (int yy = -seeRadius; yy < seeRadius; yy++) { | |
if (y + yy < 0 || y + yy >= height) continue; | |
for (int xx = -seeRadius; xx < seeRadius; xx++) { | |
if (xx * xx + yy * yy > seeRadius * seeRadius) continue; | |
if (x + xx < 0 || x + xx >= width) continue; | |
if (y + yy < 0 || y + yy >= height) continue; | |
if (canSee(x + xx, y + yy)) | |
sees[x + xx][y + yy] = seen[x + xx][y + yy] = true; | |
} | |
} | |
} | |
public void gainExperience(int e) { | |
if (experience / 20 < (experience + e) / 20) { | |
setStatus("Gained a new level.", new Color(255, 255, 0, 255)); | |
maxhealth += 2; | |
} | |
experience += e; | |
} | |
public void damage(int d, Monster m) { | |
super.damage(d, m); | |
setStatus("Hurt by an " + m.name + ".", whiteColor); | |
} | |
public void move(int direction) { | |
super.move(direction); | |
if (map[x][y] == '$') { | |
map[x][y] = floorchar; | |
int dscore = (int) (Math.random() * 20) + 2; | |
setStatus("Got $" + dscore + ".", whiteColor); | |
score += dscore; | |
} | |
if (map[x][y] == '!') { | |
map[x][y] = floorchar; | |
health += (int) (Math.random() * 5 + 5); | |
if (health > maxhealth) health = maxhealth; | |
setStatus("Drank a healing potion.", whiteColor); | |
} | |
rest++; | |
if (rest > 20) { | |
rest = 0; | |
if (health < maxhealth) | |
health++; | |
} | |
if (!running || rest % 3 == 0) | |
tick(); | |
if (energy <= 0) running = false; | |
if (running) energy--; | |
else if (rest % 3 == 0 && energy < maxEnergy) energy++; | |
drawPanel.repaint(); | |
} | |
public void goDown() { | |
monsters.removeAllElements(); | |
monsters.add(player); | |
genMap(); | |
reset(); | |
setStatus("Went downstairs.", whiteColor); | |
} | |
public void ai() { | |
} | |
} | |
class Monster { | |
int x = 10, y = 10; | |
char symbol = 'a'; | |
int health, maxhealth; | |
int strength; | |
String name; | |
int experience; | |
int facing = 1; | |
int seeRadius = 7; | |
float fov = 46; | |
double ai_interest = 0.3; | |
int tx, ty; | |
boolean memory = false; | |
public Monster(int h, int str, char s, String n) { | |
maxhealth = health = h; | |
strength = str; | |
symbol = s; | |
name = n; | |
experience = 0; | |
facing = (int) (Math.random() * 8); | |
} | |
public void place() { | |
while (true) { | |
x = (int) (Math.random() * width); | |
y = (int) (Math.random() * height); | |
if (map[x][y] == floorchar) | |
break; | |
} | |
} | |
public void see() { | |
for (int yy = -seeRadius; yy < seeRadius; yy++) { | |
if (y + yy < 0 || y + yy >= height) continue; | |
for (int xx = -seeRadius; xx < seeRadius; xx++) { | |
if (xx * xx + yy * yy > seeRadius * seeRadius) continue; | |
if (x + xx < 0 || x + xx >= width) continue; | |
if (y + yy < 0 || y + yy >= height) continue; | |
if (canSee(x + xx, y + yy)) { | |
float rr = (xx * xx + yy * yy); | |
float light = rr > 1 ? 0.5f + 4 / (rr - 1) : 1; | |
if (light > 1) light = 1; | |
if (light > monsterSees[x + xx][y + yy]) | |
monsterSees[x + xx][y + yy] = light; | |
} | |
} | |
} | |
} | |
public boolean canSee(float xb, float yb) { | |
// pick corner nearest monster | |
float x2 = xb; | |
float y2 = yb; | |
if (x2 < x) x2 += 0.5f; | |
else if (x2 > x) x2 -= 0.5f; | |
if (y2 < y) y2 += 0.5f; | |
else if (y2 > y) y2 -= 0.5f; | |
float dx = x2 - x; | |
float dy = y2 - y; | |
if (dx * dx + dy * dy > seeRadius * seeRadius) return false; | |
// field of vision | |
double dl = Math.sqrt(dx * dx + dy * dy); | |
double dot = dx / dl * unitDirections[facing][0] + dy / dl * unitDirections[facing][1]; | |
if (dot < Math.cos(fov * Math.PI / 180)) return false; | |
return lineOfSight(xb, yb); | |
} | |
public boolean lineOfSight(float xb, float yb) { | |
// pick corner nearest monster | |
float x2 = xb; | |
float y2 = yb; | |
if (x2 < x) x2 += 0.5f; | |
else if (x2 > x) x2 -= 0.5f; | |
if (y2 < y) y2 += 0.5f; | |
else if (y2 > y) y2 -= 0.5f; | |
float dx = x2 - x; | |
float dy = y2 - y; | |
float l = Math.max(Math.abs(dx), Math.abs(dy)); | |
dx /= l; | |
dy /= l; | |
float xx = x; | |
float yy = y; | |
while (l > 0) { | |
int ix = (int) (xx + 0.5); | |
int iy = (int) (yy + 0.5); | |
if (x2 == ix && y2 == iy) break; | |
if (!(x == ix && y == iy) && map[ix][iy] == '#') | |
return false; | |
xx += dx; | |
yy += dy; | |
l--; | |
} | |
return true; | |
} | |
public void gainExperience(int e) { | |
experience += e; | |
} | |
public void damage(int d, Monster m) { | |
health -= d; | |
if (health < 0) { | |
// death | |
char fl = name.charAt(0); | |
player.setStatus(((fl == 'a' /*...*/) ? "An " : "A ") + name + " dies.", whiteColor); | |
monsters.remove(this); | |
// experience | |
m.gainExperience((int) (Math.random() * strength / 2)); | |
} | |
// carnage | |
if (map[x][y] == floorchar) { | |
switch ((int) (Math.random() * 3)) { | |
case 0: | |
map[x][y] = ','; | |
break; | |
case 1: | |
map[x][y] = '\''; | |
break; | |
case 2: | |
map[x][y] = '`'; | |
break; | |
} | |
} | |
} | |
public void move(int direction) { | |
if (direction > 7) return; | |
if (direction != facing) { | |
facing = direction; | |
return; | |
} | |
move(directions[direction][0], directions[direction][1]); | |
} | |
public void move(int dx, int dy) { | |
if (dx == 0 && dy == 0) return; | |
if (x + dx < 0 || x + dx >= width) return; | |
if (y + dy < 0 || y + dy >= height) return; | |
if (map[x + dx][y + dy] == '#') return; | |
// if walk into a monster -- do damage | |
for (Monster m : monsters) { | |
if (m.x == x + dx && m.y == y + dy) { | |
m.damage((int) (Math.random() * (strength + (experience / 20))), this); | |
return; | |
} | |
} | |
x += dx; | |
y += dy; | |
} | |
public void ai() { | |
// if can see/remember player move towards them | |
boolean see = canSee(player.x, player.y); | |
if (see || memory) { | |
if (Math.random() < ai_interest / 2) return; | |
if (see) { | |
memory = true; | |
tx = player.x; | |
ty = player.y; | |
} | |
int dx = tx - x; | |
int dy = ty - y; | |
if (dx != 0 || dy != 0) { | |
// attempts to move straight or diagonally | |
int ddx = dx > 0 ? 1 : dx < 0 ? -1 : 0; | |
int ddy = dy > 0 ? 1 : dy < 0 ? -1 : 0; | |
int direction = facings[ddx + 1][ddy + 1]; | |
if (map[x + ddx][y + ddy] != '#') { | |
move(direction); | |
return; | |
} | |
int right = direction + 1; | |
if (right >= 8) right -= 8; | |
if (map[directions[right][0] + x][directions[right][1] + y] != '#') { | |
move(right); | |
return; | |
} | |
int left = direction - 1; | |
if (left < 0) left += 8; | |
if (map[directions[left][0] + x][directions[left][1] + y] != '#') { | |
move(left); | |
return; | |
} | |
} | |
} | |
if (Math.random() > ai_interest) { | |
// fake a memory to get ai to go there | |
memory = true; | |
tx = (int) (Math.random() * width); | |
ty = (int) (Math.random() * height); | |
} | |
} | |
} | |
class Room { | |
int x, y, w, h; | |
public Room() { | |
while (true) { | |
x = (int) (Math.random() * (width - 13)) + 2; | |
y = (int) (Math.random() * (height - 13)) + 2; | |
w = (int) (Math.random() * 10) + 3; | |
h = (int) (Math.random() * 10) + 3; | |
boolean clear = true; | |
for (int yy = y - 1; yy < y + h + 1; yy++) { | |
for (int xx = x - 1; xx < x + w + 1; xx++) { | |
if (xx >= width || yy >= height || map[xx][yy] == floorchar) { | |
clear = false; | |
break; | |
} | |
} | |
if (!clear) break; | |
} | |
if (!clear) continue; | |
for (int yy = y; yy < y + h; yy++) { | |
for (int xx = x; xx < x + w; xx++) { | |
map[xx][yy] = floorchar; | |
} | |
} | |
break; | |
} | |
} | |
public void createPathTo(Room b) { | |
int x = (int) (Math.random() * (w)) + this.x; | |
int y = (int) (Math.random() * (h)) + this.y; | |
int x2 = (int) (Math.random() * (b.w)) + b.x; | |
int y2 = (int) (Math.random() * (b.h)) + b.y; | |
int dx = x2 - x; | |
int dy = y2 - y; | |
dx = dx > 0 ? 1 : (dx < 0 ? -1 : 0); | |
dy = dy > 0 ? 1 : (dy < 0 ? -1 : 0); | |
boolean horizontal = (Math.random() > 0.5); | |
while (x != x2 || y != y2) { | |
if (y == y2 || (horizontal && x != x2)) | |
x += dx; | |
else | |
y += dy; | |
if (!(x > 0 && y > 0 && x < width && y < height)) break; | |
map[x][y] = floorchar; | |
if (Math.random() < 0.1) | |
horizontal = !horizontal; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment