Created
April 26, 2024 11:43
-
-
Save Stagyrite/921e5e48465e92ee0fe829b4e32b2c05 to your computer and use it in GitHub Desktop.
a level compiler inspired by openroguelike
This file contains 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
/* | |
* a level compiler inspired by openroguelike | |
* https://GitHub.com/tleguern/openroguelike | |
*/ | |
import java.io.FileWriter; | |
import java.io.IOException; | |
import java.io.InputStreamReader; | |
import java.io.PrintWriter; | |
import java.nio.charset.StandardCharsets; | |
import java.util.Random; | |
import java.util.logging.Logger; | |
import java.util.stream.IntStream; | |
import static java.util.logging.Level.FINEST; | |
import static java.util.logging.Level.SEVERE; | |
enum LevelType { | |
L_CAVE, L_ROOMS, L_STATIC | |
} | |
enum Race { | |
R_DOG('d'), R_HUMAN('@'); | |
private final char character; | |
Race(char character) { | |
this.character = character; | |
} | |
char getCharacter() { | |
return this.character; | |
} | |
} | |
enum TileType { | |
T_DOOR('+'), T_DSTAIR('>'), T_EMPTY(' '), T_USTAIR('<'), T_WALL('='); | |
private final char character; | |
TileType(char character) { | |
this.character = character; | |
} | |
char getCharacter() { | |
return this.character; | |
} | |
} | |
class Cave { | |
private static final int CAVE_STEP = 3; | |
private static final int MAX_RATIO = 10; | |
private static final int MIN_WALL = 5; | |
private static final int WALL_RATIO = 4; | |
private final Level level; | |
Cave(Level level) { | |
this.level = level; | |
} | |
private void caveInit() { | |
for (var y = 1; y < Level.ROWS - 1; y++) { | |
for (var x = 1; x < Level.COLUMNS - 1; x++) { | |
level.setTileType(x, y, randPick()); | |
} | |
} | |
for (var y = 0; y < Level.ROWS; y++) { | |
level.setTileType(0, y, TileType.T_WALL); | |
level.setTileType(Level.COLUMNS - 1, y, TileType.T_WALL); | |
} | |
for (var x = 0; x < Level.COLUMNS; x++) { | |
level.setTileType(x, 0, TileType.T_WALL); | |
level.setTileType(x, Level.ROWS - 1, TileType.T_WALL); | |
} | |
} | |
private void caveReduceNoise() { | |
for (var y = 1; y < Level.ROWS - 1; y++) { | |
for (var x = 1; x < Level.COLUMNS - 1; x++) { | |
var nwall = getNWall(x, y); | |
if (nwall >= MIN_WALL) { | |
level.setTileType(x, y, TileType.T_WALL); | |
} else { | |
level.dig(x, y); | |
} | |
} | |
} | |
} | |
void draw() { | |
caveInit(); | |
IntStream.range(0, CAVE_STEP) | |
.forEach(x -> caveReduceNoise()); | |
} | |
private int getNWall(int x, int y) { | |
var nwall = 0; | |
for (int iy = -1; iy <= 1; iy++) { | |
for (int ix = -1; ix <= 1; ix++) { | |
if (level.isDiggable(x + ix, y + iy)) { | |
nwall++; | |
} | |
} | |
} | |
return nwall; | |
} | |
private TileType randPick() { | |
var x = OpenRoguelike.getRandom(MAX_RATIO); | |
TileType tileType; | |
if (x < WALL_RATIO) { | |
tileType = TileType.T_WALL; | |
} else { | |
tileType = TileType.T_EMPTY; | |
} | |
return tileType; | |
} | |
} | |
class Creature { | |
private static final String[] NAMES = {"Fido", "Idefix", "Chien", "Rex", "Medor"}; | |
private final Race race; | |
private String name; | |
Creature(Race race) { | |
this.race = race; | |
if (race == Race.R_DOG) { | |
name = NAMES[OpenRoguelike.getRandom(NAMES.length)]; | |
} | |
} | |
String getName() { | |
return this.name; | |
} | |
Race getRace() { | |
return this.race; | |
} | |
void moveAt(Tile to) { | |
to.setCreature(this); | |
} | |
} | |
class Display { | |
private static final Logger LOG = Logger.getLogger("Display"); | |
private final char[][] window = new char[Level.ROWS][Level.COLUMNS]; | |
private void addChar(int x, int y, char c) { | |
window[y][x] = c; | |
} | |
String print(Level level) { | |
for (int y = 0; y < Level.ROWS; y++) { | |
for (int x = 0; x < Level.COLUMNS; x++) { | |
tilePrint(level.getTile(x, y), x, y); | |
} | |
} | |
return refresh(); | |
} | |
private String refresh() { | |
var sb = new StringBuilder(); | |
for (var y = 0; y < Level.ROWS; y++) { | |
for (var x = 0; x < Level.COLUMNS; x++) { | |
sb.append(this.window[y][x]); | |
} | |
sb.append('\n'); | |
} | |
return sb.toString(); | |
} | |
private void tilePrint(Tile tile, int x, int y) { | |
var type = tile.getType(); | |
addChar(x, y, type.getCharacter()); | |
var creature = tile.getCreature(); | |
if (creature != null) { | |
var race = creature.getRace(); | |
addChar(x, y, race.getCharacter()); | |
var debugMessage = String.format("%s @ (%d, %d)", creature.getName(), x, y); | |
LOG.log(FINEST, debugMessage); | |
} | |
} | |
} | |
class Level { | |
static final int COLUMNS = 80; | |
static final int ROWS = 20; | |
private final Tile[][] tile = new Tile[ROWS][COLUMNS]; | |
Level() { | |
for (var y = 0; y < ROWS; y++) { | |
for (var x = 0; x < COLUMNS; x++) { | |
tile[y][x] = new Tile(); | |
} | |
} | |
} | |
private void addStair(TileType tileType) { | |
var x = OpenRoguelike.getRandom(COLUMNS); | |
var y = OpenRoguelike.getRandom(ROWS); | |
if (getTileType(x, y) == TileType.T_EMPTY) { | |
setTileType(x, y, tileType); | |
} else { | |
addStair(tileType); | |
} | |
} | |
void dig(int x, int y) { | |
setTileType(x, y, TileType.T_EMPTY); | |
} | |
void generate(LevelType levelType, String arg) throws IOException { | |
switch (levelType) { | |
case L_CAVE: | |
Cave cave = new Cave(this); | |
cave.draw(); | |
break; | |
case L_ROOMS: | |
Rooms rooms = new Rooms(this); | |
rooms.draw(); | |
break; | |
case L_STATIC: | |
LevelStatic levelStatic = new LevelStatic(this); | |
levelStatic.draw(arg); | |
break; | |
} | |
if (levelType != LevelType.L_STATIC) { | |
addStair(TileType.T_USTAIR); | |
addStair(TileType.T_DSTAIR); | |
} | |
} | |
Tile getTile(int x, int y) { | |
return this.tile[y][x]; | |
} | |
TileType getTileType(int x, int y) { | |
return this.tile[y][x].getType(); | |
} | |
boolean isDiggable(int x, int y) { | |
return getTileType(x, y) == TileType.T_WALL; | |
} | |
void setTileType(int x, int y, TileType type) { | |
this.tile[y][x].setType(type); | |
} | |
} | |
class LevelStatic { | |
private static final Logger LOG = Logger.getLogger("LevelStatic"); | |
private final Level level; | |
LevelStatic(Level level) { | |
this.level = level; | |
} | |
void draw(String filename) throws IOException { | |
var buf = new char['ـ']; | |
readFile(buf, filename); | |
for (var y = 0; y < Level.ROWS; y++) { | |
for (var x = 0; x < Level.COLUMNS; x++) { | |
var tile = buf[x + y * Level.COLUMNS]; | |
switch (tile) { | |
case ' ': | |
level.dig(x, y); | |
break; | |
case '1': | |
level.setTileType(x, y, TileType.T_WALL); | |
break; | |
case '2': | |
level.setTileType(x, y, TileType.T_DOOR); | |
break; | |
case '3': | |
level.setTileType(x, y, TileType.T_DSTAIR); | |
break; | |
case '\0': | |
case '\r': | |
case '\n': | |
break; | |
default: | |
LOG.log(SEVERE, "{0}: unknown tile", (int) tile); | |
break; | |
} | |
} | |
} | |
} | |
private void readFile(char[] buf, String filename) throws IOException { | |
var clazz = getClass(); | |
var stream = clazz.getResourceAsStream(filename); | |
if (stream == null) { | |
LOG.log(SEVERE, "{0}: file not found", filename); | |
} else { | |
try (var reader = new InputStreamReader(stream, StandardCharsets.US_ASCII)) { | |
if (reader.read(buf) <= 0) { | |
LOG.log(SEVERE, "{0}: file is empty", filename); | |
} | |
} | |
} | |
} | |
} | |
class LevelCompiler { | |
static void write(String filename) throws IOException { | |
var out = new FileWriter(filename); | |
try (var writer = new PrintWriter(out)) { | |
write(writer); | |
} | |
} | |
private static void write(PrintWriter out) throws IOException { | |
var level = new Level(); | |
level.generate(LevelType.L_CAVE, null); | |
out.println(""); | |
out.println("static char * level[] = {"); | |
out.println("\"80 20 2 1\""); | |
out.println("\" c black\""); | |
out.println("\"# c white\""); | |
writeLines(level, out); | |
out.println("\n};"); | |
} | |
private static void writeLines(Level level, PrintWriter out) { | |
var line = new char[Level.COLUMNS]; | |
for (var y = 0; y < Level.ROWS; y++) { | |
if (y != 0) { | |
out.println(','); | |
} | |
for (var x = 0; x < Level.COLUMNS; x++) { | |
if (level.isDiggable(x, y)) { | |
line[x] = '#'; | |
} else { | |
line[x] = ' '; | |
} | |
} | |
out.print("\""); | |
out.print(line); | |
out.print("\""); | |
} | |
} | |
} | |
public class OpenRoguelike { | |
private static final Logger LOG = Logger.getLogger("OpenRoguelike"); | |
private static final int ARGS_LENGTH = 2; | |
private static final int CAVE_RATIO = 9; | |
private static final int DOG_POSITION = 16; | |
private static final int EOF = -1; | |
private static final int MAX_RATIO = 10; | |
private static final int PLAYER_POSITION = 15; | |
private static final Random random = new Random(); | |
static int getRandom(int bound) { | |
return random.nextInt(bound); | |
} | |
static String generate(Creature dog, LevelType levelType, String arg) throws IOException { | |
Creature player = new Creature(Race.R_HUMAN); | |
Level level = new Level(); | |
do { | |
level.generate(levelType, arg); | |
} while (level.getTileType(DOG_POSITION, DOG_POSITION) != TileType.T_EMPTY || level.getTileType(PLAYER_POSITION, PLAYER_POSITION) != TileType.T_EMPTY); | |
player.moveAt(level.getTile(PLAYER_POSITION, PLAYER_POSITION)); | |
dog.moveAt(level.getTile(DOG_POSITION, DOG_POSITION)); | |
var display = new Display(); | |
return display.print(level); | |
} | |
static String generate(LevelType levelType, String arg) throws IOException { | |
var dog = new Creature(Race.R_DOG); | |
return generate(dog, levelType, arg); | |
} | |
static void play() throws IOException { | |
var console = System.console(); | |
if (console == null) { | |
LOG.log(SEVERE, "no system console"); | |
} else { | |
var writer = console.writer(); | |
writer.println(generate(LevelType.L_STATIC, "town.lvl")); | |
var reader = console.reader(); | |
while (reader.read() != EOF) { | |
writer.println(generate(randPick(), null)); | |
} | |
} | |
} | |
public static void main(String[] args) throws IOException { | |
if (args.length > 0) { | |
if (args.length == ARGS_LENGTH && !"-o".equals(args[0])) { | |
LOG.log(SEVERE, "usage: java OpenRoguelike [-o output]"); | |
} else { | |
LevelCompiler.write(args[1]); | |
} | |
} else { | |
play(); | |
} | |
} | |
private static LevelType randPick() { | |
var x = OpenRoguelike.getRandom(MAX_RATIO); | |
LevelType levelType; | |
if (x < CAVE_RATIO) { | |
levelType = LevelType.L_CAVE; | |
} else { | |
levelType = LevelType.L_ROOMS; | |
} | |
return levelType; | |
} | |
} | |
class Rooms { | |
private static final int HUNDRED = 100; | |
private static final int MAX_COUNT = 4; | |
private static final int TEN = 10; | |
private final Level level; | |
private int count; | |
private int dir; | |
Rooms(Level level) { | |
this.level = level; | |
} | |
private static int tenPercentOf(int x) { | |
return TEN * x / HUNDRED; | |
} | |
void draw() { | |
init(); | |
split(0, Level.COLUMNS, 0, Level.ROWS); | |
} | |
private void init() { | |
for (var y = 0; y < Level.ROWS; y++) { | |
for (var x = 0; x < Level.COLUMNS; x++) { | |
level.setTileType(x, y, TileType.T_WALL); | |
} | |
} | |
} | |
private void split(int minx, int x, int miny, int y) { | |
dir ^= 0x1; | |
if (count == MAX_COUNT) { | |
return; | |
} | |
int ly; | |
int lx; | |
if (dir == 1) { | |
lx = minx; | |
int bound = Math.max(1, y - miny); | |
ly = miny + OpenRoguelike.getRandom(bound); | |
for (; lx < x; lx++) { | |
level.dig(lx, ly); | |
} | |
lx = 0; | |
} else { | |
int ten = tenPercentOf(x); | |
ly = miny; | |
lx = ten + OpenRoguelike.getRandom(x - ten * 2); | |
for (; ly < y; ly++) { | |
level.dig(lx, ly); | |
} | |
ly = 0; | |
} | |
count += 1; | |
if (lx == 0) { | |
split(minx, x, miny, ly); | |
} else { | |
split(minx, lx, miny, y); | |
} | |
if (ly == 0) { | |
split(lx, x, miny, y); | |
} else { | |
split(minx, x, ly, y); | |
} | |
} | |
} | |
class Tile { | |
private Creature creature; | |
private TileType type; | |
Tile() { | |
this.type = TileType.T_EMPTY; | |
} | |
Creature getCreature() { | |
return this.creature; | |
} | |
void setCreature(Creature creature) { | |
this.creature = creature; | |
} | |
TileType getType() { | |
return this.type; | |
} | |
void setType(TileType type) { | |
this.type = type; | |
} | |
} |
This file contains 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
1111111111111111111111111111111111111111111111111111111111111111111111111111111 | |
1 1 | |
1 1 | |
1 1 | |
1 1 | |
1 1111111111111 1111111111111 1 | |
1 1 1 1 13 1 | |
1 1 1 1 1 1 | |
1 1 1 1 1 1 | |
1 1 1 1 1 1 | |
1 1121111111 1 1 1 1 1 | |
1 1 1 1 1 1 1 1 1 | |
1 1 2 2 2 2 2 1 1 | |
1 1 1 1 1 1 1 1 1 | |
1 1111111111 1111111111111 1111111111111 1 | |
1 1 | |
1 1 | |
1 1 | |
1111111111111111111111111111111111111111111111111111111111111111111111111111111 |
There are also levels with rooms, and here's an example.
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
====== ================== ======================================================
@ ======================================================
d > <
================================================================================
================================================================================
================================================================================
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's yet another cave level.