diff --git a/.gitignore b/.gitignore index 7c08765..26673c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /**/.idea/ /**/build +/**/*.vpp.* +/**/*.iml +/**/*.class +/**/target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e291428 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# au_software_design + +##1. Shell + +###Command line utility similar to Unix shell. + +####Supported features: + +* echo command +* cat command +* wc command +* exit command +* pwd command +* environment variables +* unknown commands are passed to system shell as separate process through Java.Process library. + +####Class diagram: +![shell class diagram](https://www.gliffy.com/go/share/image/smx5dub0j39jxied850w.png?utm_medium=live-embed&utm_source=custom) + +####Data flow: + * Main: run Shell object. + * Shell: Read line from System.in. +   + * Preprocessor: Substitute environment variables in input string. E. g. "Hello, $name" -> "Hello, Alex" + * Tokeniser: Split string into a list of tokens: words and operators. + * Parser: Parse list of tokens as sequence of commands divided by pipes. + * Command Executor: Perform chained computation passing output of one comand as input to the next one. + * Pass result to System.in and loop again. + + +##2. Grep +###Command similar to UNIX grep utility. + +####Supports: +* Reading from standard input or from file. +* Parameters: + * -i: case insensivity. + * -w: whole words search. + * -A %number%: Number of lines to add to output after each matched line. + +####Implementation: +* For regular expressions java.util.regex package is used. + +* For parsing command line arguments Apache Commons CLI library was chosen, because it is pretty easy to use and fits our requirements very well. Other libraries like JCommander and args4j were considered as an option, but were rejected as a little overkill at this point. If number of arguments will grow seriously, it may be reasonable to switch to one of these libraries, because of usage of annotations for introducing parameters. diff --git a/book_store/book_store_class_diagram.vpp b/book_store/book_store_class_diagram.vpp new file mode 100644 index 0000000..b71e5cc Binary files /dev/null and b/book_store/book_store_class_diagram.vpp differ diff --git a/roguelike/arch/roguelike_class.svg b/roguelike/arch/roguelike_class.svg new file mode 100644 index 0000000..a71478c --- /dev/null +++ b/roguelike/arch/roguelike_class.svg @@ -0,0 +1,705 @@ + + +Models-health : int-level : int-equipment : List<Item>BeingMob-name : String-inventory : List<Item>Player-DRAGON-GHOST-PRINCESS<<enumeration>>MobType-map : Tile[][]-mobs : List<Pair<Mob, Position...-player : Pair<Player, Position>GameState-x : int-y : intPositionTile-STONE-FIRE-FLOOR<<enumeration>>TileType-red : int-blue : int-green : intColor-level : intItemControllerLogic-WEAPON-WEARING<<enumeration>>ItemType+run()Game+iterateState()+iterateMobs()+updatePlayerPosition()+handleCollisions()+collectItem()+talkToPrincess()+meetWithGhost()+fight()+die()Logic-framerate : int+renderFrame()+getInputs()+schedule()Scheduler-RIGHT-LEFT-UP-DOWN<<enumeration>>Direction*111111*1*111Powered ByVisual Paradigm Community Edition diff --git a/roguelike/arch/roguelike_class_diagram.vpp b/roguelike/arch/roguelike_class_diagram.vpp new file mode 100644 index 0000000..bfe5c63 Binary files /dev/null and b/roguelike/arch/roguelike_class_diagram.vpp differ diff --git a/roguelike/arch/roguelike_component.svg b/roguelike/arch/roguelike_component.svg new file mode 100644 index 0000000..324649f --- /dev/null +++ b/roguelike/arch/roguelike_component.svg @@ -0,0 +1,183 @@ + + +<<component>>Models<<component>>Logic<<component>>ControllerLogicGameStatePowered ByVisual Paradigm Community Edition diff --git a/roguelike/impl/README.md b/roguelike/impl/README.md new file mode 100644 index 0000000..0d9c317 --- /dev/null +++ b/roguelike/impl/README.md @@ -0,0 +1,15 @@ +## ROGUELIKE +### Управление +1. Передвижение: клавиши A, W, S, D +2. В стенах можно копать ходы - для этого надо стоя рядом с ней шагнуть в ее сторону еще раз и нажать [enter] +3. Смена оружия: клавиша I +### Мобы +1. Гриб (M): Статический моб, отравляет территорию вокруг себя. +2. Призрак (G): Когда оказывается рядом с игроком, начинает за ним гнаться, и если догоняет, то инвертирует клавиши управления и начинает убегать. Если после этого игрок догоняет призрака, то призрак исчезает и управление возвращается к исходному. +3. Дракон (D): Случайно блуждает по карте, поджигая за собой пол. Можно сразиться с драконом, встав рядом, и зажав [enter], и получить от него оружие более высокого уровня (но это не точно). После победы над всеми драконами на уровне открывается переход на новый уровень (Z), который надо найти на карте. +### Предметы +По карте разбросаны: +* Аптечки +* Оружие +### Уровни +Каждый новый уровень отличается от предыдущего количеством урона, которое игрок наносит драконам или получает при схватках и хождении по отравленным или подожженным тайлам. При этом максимальный уровень здоровья и его количество, восстанавливаемое одной аптечкой не изменяются. diff --git a/roguelike/impl/pom.xml b/roguelike/impl/pom.xml new file mode 100644 index 0000000..d7ae642 --- /dev/null +++ b/roguelike/impl/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + simiyutin + roguelike + 1.0-SNAPSHOT + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.0.0 + + + jar-with-dependencies + + + + com.simiyutin.au.roguelike.Main + + + + + + package + + single + + + + + + + + + my-local-repo + file://${project.basedir}/repo + + + + + net.trystan + ascii-panel + 1.1 + + + junit + junit + 4.12 + + + org.apache.logging.log4j + log4j-api + 2.8.1 + + + org.apache.logging.log4j + log4j-core + 2.8.1 + + + + com.google.guava + guava + 22.0 + + + + \ No newline at end of file diff --git a/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.jar b/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.jar new file mode 100644 index 0000000..5554ad9 Binary files /dev/null and b/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.jar differ diff --git a/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.pom b/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.pom new file mode 100644 index 0000000..054d201 --- /dev/null +++ b/roguelike/impl/repo/net/trystan/ascii-panel/1.1/ascii-panel-1.1.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + net.trystan + ascii-panel + 1.1 + POM was created from install:install-file + diff --git a/roguelike/impl/repo/net/trystan/ascii-panel/maven-metadata-local.xml b/roguelike/impl/repo/net/trystan/ascii-panel/maven-metadata-local.xml new file mode 100644 index 0000000..316bbe9 --- /dev/null +++ b/roguelike/impl/repo/net/trystan/ascii-panel/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + net.trystan + ascii-panel + + 1.1 + + 1.1 + + 20170513231215 + + diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Main.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Main.java new file mode 100644 index 0000000..ff0dbc1 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Main.java @@ -0,0 +1,16 @@ +package com.simiyutin.au.roguelike; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +public class Main { + private static final Logger LOGGER = LogManager.getLogger(Main.class); + + public static void main(String[] args) { + LOGGER.trace("application started"); + Scheduler scheduler = new Scheduler(); + LOGGER.trace("start scheduling"); + scheduler.schedule(); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Scheduler.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Scheduler.java new file mode 100644 index 0000000..8ebd91c --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/Scheduler.java @@ -0,0 +1,77 @@ +package com.simiyutin.au.roguelike; + +import asciiPanel.AsciiPanel; +import com.simiyutin.au.roguelike.screens.Screen; +import com.simiyutin.au.roguelike.screens.StartScreen; +import com.simiyutin.au.roguelike.util.RecurringTask; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.lang.reflect.InvocationTargetException; + + +/** + * Routes user input to current Screen object, iterates Screen periodically and repaints it. + */ +public class Scheduler extends JFrame implements KeyListener { + private static final Logger LOGGER = LogManager.getLogger(Scheduler.class); + private AsciiPanel terminal; + private Screen screen; + + public Scheduler() { + super(); + + this.terminal = new AsciiPanel(); + add(terminal); + pack(); + + this.screen = new StartScreen(); + + addKeyListener(this); + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setVisible(true); + } + + private void iterate() { + try { + SwingUtilities.invokeAndWait(() -> { + screen = screen.updateState(); + repaint(); + }); + } catch (InterruptedException | InvocationTargetException e) { + LOGGER.error(e); + } + } + + /** + * Begin iterating state. + */ + public void schedule() { + new RecurringTask(this::iterate, 1000 / 60); + } + + @Override + public void keyTyped(KeyEvent e) { + + } + + @Override + public void keyPressed(KeyEvent e) { + screen = screen.respondToUserInput(e); + } + + @Override + public void keyReleased(KeyEvent e) { + + } + + @Override + public void repaint() { + terminal.clear(); + screen.display(terminal); + super.repaint(); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Action.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Action.java new file mode 100644 index 0000000..ab04de0 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Action.java @@ -0,0 +1,9 @@ +package com.simiyutin.au.roguelike.models; + + +/** + * Action that player object will do after human player pressed [enter] + */ +public interface Action { + void act(); +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Position.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Position.java new file mode 100644 index 0000000..fc574f2 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Position.java @@ -0,0 +1,15 @@ +package com.simiyutin.au.roguelike.models; + + +/** + * Position itself. + */ +public class Position { + public int x; + public int y; + + public Position(int x, int y) { + this.x = x; + this.y = y; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Tile.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Tile.java new file mode 100644 index 0000000..8640ba7 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/Tile.java @@ -0,0 +1,57 @@ +package com.simiyutin.au.roguelike.models; + +import asciiPanel.AsciiPanel; + +import java.awt.*; + + +/** + * Describes tiles the world can be built from. + */ +public enum Tile { + FLOOR('.', AsciiPanel.yellow, 0, true), + POISONED_FLOOR('.', Color.GREEN, -5, true), + INFLAMED_FLOOR('.', Color.RED, -5, true), + WALL('#', AsciiPanel.yellow, 0, false), + Z_TELEPORT('Z', AsciiPanel.brightMagenta, 0, true), + BOUNDS('x', AsciiPanel.brightBlack, 0, false); + + private char glyph; + private Color color; + private int deltaHealth; + private boolean isWalkable; + + Tile(char glyph, Color color, int deltaHealth, boolean isWalkable) { + this.glyph = glyph; + this.color = color; + this.deltaHealth = deltaHealth; + this.isWalkable = isWalkable; + } + + public char getGlyph() { + return glyph; + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + /** + * @return amount of health that will be applied to player's health after stepping onto tile. + */ + public int getDeltaHealth() { + return deltaHealth; + } + + public boolean isWalkable() { + return isWalkable; + } + + public boolean isDiggable() { + return this == WALL; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/World.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/World.java new file mode 100644 index 0000000..855838a --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/World.java @@ -0,0 +1,207 @@ +package com.simiyutin.au.roguelike.models; + + +import com.simiyutin.au.roguelike.models.beings.Being; +import com.simiyutin.au.roguelike.models.beings.Player; +import com.simiyutin.au.roguelike.models.items.ThrownItem; +import com.simiyutin.au.roguelike.util.DelayedTask; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + + +/** + * Holds map, player, mobs and loot. + */ +public class World { + + private WorldData d; + + public World(Tile[][] tiles) { + this.d = new WorldData(); + this.d.tiles = tiles; + this.d.width = tiles.length; + this.d.height = tiles[0].length; + + this.d.mobs = new ArrayList<>(); + this.d.thrownItems = new ArrayList<>(); + this.d.minLevel = 1; + this.d.player = new Player(this); + } + + public List getThrownItems() { + return d.thrownItems; + } + + /** + * minLevel defines minimum level of items and mobs. + */ + public int getMinLevel() { + return d.minLevel; + } + + public void setMinLevel(int minLevel) { + this.d.minLevel = minLevel; + } + + /** + * @return message that will be displayed on top of a screen. + */ + public String getMessage() { + return d.message; + } + + public void setMessage(String message) { + this.d.message = message; + new DelayedTask(() -> { + if (this.d.message.equals(message)) { + this.d.message = ""; + } + }, 1000); + } + + /** + * @return Tile at given position. + */ + public Tile getTile(int x, int y) { + if (x < 0 || x > getWidth() - 1 || y < 0 || y > getHeight() - 1) { + return Tile.BOUNDS; + } + return d.tiles[x][y]; + } + + public void setTile(int x, int y, Tile tile) { + d.tiles[x][y] = tile; + } + + /** + * Print current map co console. + */ + public void printTiles() { + for (int i = 0; i < d.width; i++) { + for (int j = 0; j < d.height; j++) { + System.out.print(d.tiles[j][i].getGlyph() + " "); + } + System.out.println(); + } + } + + /** + * @return glyph of the tile at given position. + */ + public char getGlyph(int x, int y) { + return getTile(x, y).getGlyph(); + } + + /** + * @return color of the tile at given position. + */ + public Color getColor(int x, int y) { + return getTile(x, y).getColor(); + } + + public int getWidth() { + return d.width; + } + + public int getHeight() { + return d.height; + } + + public Player getPlayer() { + return d.player; + } + + public void setPlayer(Player player) { + d.player = player; + } + + public List getMobs() { + return d.mobs; + } + + /** + * @return mob at given position. (except player) + */ + public Being getMob(int x, int y) { + for (Being b : getMobs()) { + if (b.x == x && b.y == y) { + return b; + } + } + return null; + } + + /** + * @return thrown item at given position. + */ + public ThrownItem getItem(int x, int y) { + for (ThrownItem b : getThrownItems()) { + if (b.x == x && b.y == y) { + return b; + } + } + return null; + } + + /** + * @return walkable position free of mobs. + */ + public Position getEmptyPosition() { + Random randomGen = new Random(); + + int x; + int y; + + do { + + x = randomGen.nextInt(getWidth()); + y = randomGen.nextInt(getHeight()); + + } while (!isEmptyFloor(x, y)); + + return new Position(x, y); + } + + /** + * Set all tiles in given radius to given type. + */ + public void setTilesAround(int x, int y, int radius, Tile tile) { + for (int i = -radius; i <= radius; i++) { + for (int j = -radius; j <= radius; j++) { + int wx = x + i; + int wy = y + j; + if (i * i + j * j <= radius * radius && getTile(wx, wy) == Tile.FLOOR) { + setTile(wx, wy, tile); + } + } + } + } + + /** + * move data from another world to current. + */ + public void moveDataFrom(World other) { + d = other.d; + } + + + private boolean isEmptyFloor(int x, int y) { + return getTile(x, y) == Tile.FLOOR + && getMobs().stream().noneMatch(b -> b.x == x && b.y == y) + && getThrownItems().stream().noneMatch(b -> b.x == x && b.y == y); + } + + private static class WorldData { + private Tile[][] tiles; + private int width; + private int height; + private Player player; + private List mobs; + private String message = ""; + private List thrownItems; + private int minLevel; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ActiveBeing.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ActiveBeing.java new file mode 100644 index 0000000..e8b1c43 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ActiveBeing.java @@ -0,0 +1,91 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.simiyutin.au.roguelike.models.World; + +import java.util.Random; + + +/** + * Beings that can move and interact with environment + */ +public abstract class ActiveBeing extends Being { + + protected int xDirection; + protected int yDirection; + protected int health; + protected int level; + protected boolean immobilized; + protected boolean alive; + private SideEffect effect; + + public ActiveBeing(World world) { + super(world); + this.health = 100; + this.effect = SideEffect.IDENTITY; + this.level = world.getMinLevel(); + this.immobilized = false; + this.alive = true; + } + + public boolean isAlive() { + return alive; + } + + public void setAlive(boolean alive) { + this.alive = alive; + } + + public void setImmobilized(boolean immobilized) { + this.immobilized = immobilized; + } + + public int getLevel() { + return level; + } + + public int getHealth() { + return health; + } + + public void setHealth(int health) { + this.health = health; + } + + /** + * Try to move being by given delta + */ + public void move(int dx, int dy) { + if (immobilized) { + return; + } + xDirection = Math.max(0, Math.min(x + dx, world.getWidth() - 1)); + yDirection = Math.max(0, Math.min(y + dy, world.getHeight() - 1)); + if (canMove(xDirection, yDirection)) { + x = xDirection; + y = yDirection; + } + + interactWithEnvironment(); + } + + public SideEffect getEffect() { + return effect; + } + + protected void setEffect(SideEffect effect) { + this.effect = effect; + } + + /** + * Check if map at given position is walkable and free from other mobs + */ + public boolean canMove(int xTo, int yTo) { + return world.getTile(xTo, yTo).isWalkable() && world.getMob(xTo, yTo) == null; + } + + public abstract void interactWithEnvironment(); + + protected void moveRandom() { + move(new Random().nextInt(3) - 1, new Random().nextInt(3) - 1); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ArtificialIntelligence.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ArtificialIntelligence.java new file mode 100644 index 0000000..1122d92 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/ArtificialIntelligence.java @@ -0,0 +1,9 @@ +package com.simiyutin.au.roguelike.models.beings; + + +/** + * Specifies way for AI to act + */ +public interface ArtificialIntelligence { + void move(); +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Being.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Being.java new file mode 100644 index 0000000..5232f53 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Being.java @@ -0,0 +1,36 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.simiyutin.au.roguelike.models.Position; +import com.simiyutin.au.roguelike.models.World; + +import java.awt.*; + + +/** + * Base being class. This beings can only exist at given position/ + */ +public abstract class Being { + protected final World world; + public int x; + public int y; + protected char glyph; + protected Color color; + + public Being(World world) { + this.world = world; + + Position position = world.getEmptyPosition(); + this.x = position.x; + this.y = position.y; + } + + public char getGlyph() { + return glyph; + } + + public Color getColor() { + return color; + } + + +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Dragon.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Dragon.java new file mode 100644 index 0000000..7ac590b --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Dragon.java @@ -0,0 +1,50 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.util.RecurringTask; + +import java.awt.*; + + +/** + * Walking mob with AI. It moves randomly, inflames floor below. When getting close, player can attack dragon, + * and if wins, gets weapon. + */ +public class Dragon extends ActiveBeing implements ArtificialIntelligence { + + public static boolean isSelfActing = true; + + public Dragon(World world) { + super(world); + + this.glyph = 'D'; + this.color = Color.RED; + inflameTiles(); + + + if (isSelfActing) { + new RecurringTask(this::move, 1000); + } + } + + @Override + public void move() { + moveRandom(); + } + + @Override + public void move(int dx, int dy) { + super.move(dx, dy); + inflameTiles(); + } + + @Override + public void interactWithEnvironment() { + + } + + private void inflameTiles() { + world.setTilesAround(x, y, 0, Tile.INFLAMED_FLOOR); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Ghost.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Ghost.java new file mode 100644 index 0000000..ae0df25 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Ghost.java @@ -0,0 +1,104 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.util.RecurringTask; + +import java.awt.*; + +import static com.simiyutin.au.roguelike.models.beings.SideEffect.IDENTITY; +import static com.simiyutin.au.roguelike.models.beings.SideEffect.INVERSED; + + +/** + * Walking mob with AI. It moves randomly, if smells player, chases him. + * On success, Ghost inverts controls of a player and starts to run away. So do all other Ghosts on the map . + */ +public class Ghost extends ActiveBeing implements ArtificialIntelligence { + + public static final int SMELL_RANGE = 15; + public static final int TRIGGER_RANGE = 3; + public static boolean isSelfActing = true; + private static boolean moveTo = true; + + public Ghost(World world) { + super(world); + + this.glyph = 'G'; + this.color = Color.cyan; + + if (isSelfActing) { + new RecurringTask(this::move, 200); + } + } + + @Override + public void interactWithEnvironment() { + if (distToPlayer(x, y) < TRIGGER_RANGE) { + SideEffect effect = world.getPlayer().getEffect(); + if (effect == INVERSED) { + world.getMobs().remove(this); + world.getPlayer().setEffect(IDENTITY); + world.setMessage("Controls back to normal"); + alive = false; + } else { + world.getPlayer().setEffect(INVERSED); + world.setMessage("Controls inversed!"); + } + moveTo = !moveTo; + } + } + + @Override + public void move() { + + if (!alive) return; + + if (distToPlayer(x, y) < SMELL_RANGE) { + if (moveTo) { + moveToPlayer(); + } else { + moveFromPlayer(); + } + } else { + moveRandom(); + } + + } + + private void moveToPlayer() { + int dx = 0; + int dy = 0; + double minDist = Double.POSITIVE_INFINITY; + for (int i = -1; i < 2; i++) { + for (int j = -1; j < 2; j++) { + if (distToPlayer(x + i, y + j) < minDist && canMove(x + i, y + j)) { + minDist = distToPlayer(x + i, y + j); + dx = i; + dy = j; + } + } + } + move(dx, dy); + } + + private void moveFromPlayer() { + int dx = 0; + int dy = 0; + double maxDist = 0; + for (int i = -1; i < 2; i++) { + for (int j = -1; j < 2; j++) { + if (distToPlayer(x + i, y + j) > maxDist && canMove(x + i, y + j)) { + maxDist = distToPlayer(x + i, y + j); + dx = i; + dy = j; + } + } + } + move(dx, dy); + } + + + private double distToPlayer(int fromX, int fromY) { + return Math.hypot(fromX - world.getPlayer().x, fromY - world.getPlayer().y); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Mushroom.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Mushroom.java new file mode 100644 index 0000000..7f4ab47 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Mushroom.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; + +import java.awt.*; + + +/** + * Static being. Poisons tiles around self and does nothing else. + */ +public class Mushroom extends Being { + + public Mushroom(World world) { + super(world); + + this.glyph = 'T'; + this.color = Color.GREEN; + poisonTiles(); + + } + + private void poisonTiles() { + world.setTilesAround(x, y, 3, Tile.POISONED_FLOOR); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Player.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Player.java new file mode 100644 index 0000000..210051a --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/Player.java @@ -0,0 +1,210 @@ +package com.simiyutin.au.roguelike.models.beings; + +import com.google.common.collect.Iterators; +import com.simiyutin.au.roguelike.models.Action; +import com.simiyutin.au.roguelike.models.Position; +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.items.*; +import com.simiyutin.au.roguelike.util.DelayedTask; +import com.simiyutin.au.roguelike.util.RecurringTask; +import com.simiyutin.au.roguelike.util.WorldFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + + +/** + * Player himself. Can move around, pick up weapons and health packs, dig through walls and attack dragons. + */ +public class Player extends ActiveBeing { + + private static final Logger LOGGER = LogManager.getLogger(Player.class); + private Weapon weapon; + private Iterator weaponIterator; + private Action action; + + public Player(World world) { + super(world); + + this.glyph = 'X'; + this.color = Color.BLUE; + this.weapon = WeaponType.HAND.getItem(); + this.weaponIterator = Iterators.cycle(this.weapon); + this.action = new RegularAction(); + } + + + /** + * Each level of player is followed by world of incremented level. + */ + public void levelUp() { + level++; + World newWorld = WorldFactory.getDefaultConfigurationOfMinLevel(world.getMinLevel() + 1); + Position pos = newWorld.getEmptyPosition(); + x = pos.x; + y = pos.y; + newWorld.setPlayer(this); + world.moveDataFrom(newWorld); + LOGGER.trace("player level upped"); + } + + + /** + * Do something on pressing [enter] key. "something" is taken from context. + */ + public void act() { + action.act(); + } + + public Weapon getWeapon() { + return weapon; + } + + public void changeWeapon() { + weapon = weaponIterator.next(); + } + + private void addWeapon(Weapon newWeapon) { + List weapons = linearizeWeapons(); + if (!weapons.contains(newWeapon)) { + weapons.add(newWeapon); + } + weaponIterator = Iterators.cycle(weapons); + changeWeapon(); + } + + private List linearizeWeapons() { + List list = new ArrayList<>(); + + + list.add(weapon); + Weapon next = weaponIterator.next(); + while (next != weapon) { + list.add(next); + next = weaponIterator.next(); + } + + return list; + } + + @Override + public void interactWithEnvironment() { + int deltaHealth = world.getTile(x, y).getDeltaHealth(); + health += deltaHealth * level; + + ThrownItem thrownItem = world.getItem(x, y); + if (thrownItem != null) { + Item item = thrownItem.getItem(); + if (item instanceof Weapon) { + LOGGER.trace(String.format("picked up weapon: %s", item.getName())); + addWeapon((Weapon) item); + world.getThrownItems().removeIf(w -> + w.getItem() instanceof Weapon && ((Weapon) w.getItem()).getLevel() <= weapon.getLevel()); + } else if (item instanceof MedAid) { + LOGGER.trace("picked up med aid"); + health += ((MedAid) item).getValue(); + health = Math.min(health, 100); + } + world.setMessage(String.format("picked %s", item.getName())); + world.getThrownItems().remove(thrownItem); + } + + Being being = getMobNearMe(); + if (being != null) { + if (being instanceof Dragon) { // todo inheritance + world.setMessage("press [enter] to start battle with dragon"); + } + } + + if (world.getTile(x, y) == Tile.Z_TELEPORT) { + levelUp(); + } + } + + private Being getMobNearMe() { + for (Being b : world.getMobs()) { + if (distTo(b.x, b.y) == 1) { + return b; + } + } + + return null; + } + + private void startBattleWithDragon(Dragon dragon) { + setImmobilized(true); + dragon.setImmobilized(true); + action = new AttackingAction(dragon); + LOGGER.trace("started battle with dragon"); + new RecurringTask(() -> { + if (dragon.isAlive()) { + int harm = dragon.getLevel(); + setHealth(health - harm); + } + }, 500); + } + + private double distTo(int toX, int toY) { + return Math.hypot(toX - x, toY - y); + } + + /** + * Describes how to act in normal mode. + */ + class RegularAction implements Action { + + @Override + public void act() { + if (world.getTile(xDirection, yDirection).isDiggable()) { + world.setTile(xDirection, yDirection, Tile.FLOOR); + } + + Being being = getMobNearMe(); + if (being != null) { + if (being instanceof Dragon) { // todo inheritance + startBattleWithDragon((Dragon) being); + } + } + } + } + + /** + * Describes how to act in attacking mode. + */ + class AttackingAction implements Action { + private ActiveBeing enemy; + + public AttackingAction(ActiveBeing enemy) { + this.enemy = enemy; + } + + @Override + public void act() { + int harm = level * weapon.getLevel() + weapon.getHarm(); + int health = enemy.getHealth(); + enemy.setHealth(health - harm); + world.setMessage(String.format("enemy health: %d", enemy.getHealth())); + if (enemy.getHealth() < 0) { + world.setMessage("You won!"); + LOGGER.trace("player won battle with dragon"); + Weapon newWeapon = Weapon.getRandomOfLevel(enemy.getLevel() + 2); + addWeapon(newWeapon); + new DelayedTask(() -> world.setMessage(String.format("Obtained %s", newWeapon.getName())), 1000); + world.getMobs().remove(enemy); + enemy.setAlive(false); + setImmobilized(false); + action = new RegularAction(); + if (world.getMobs().stream().noneMatch(m -> m instanceof Dragon)) { + world.setMessage("Killed last dragon. Find Z tile to proceed to next level"); + Position position = world.getEmptyPosition(); + world.setTile(position.x, position.y, Tile.Z_TELEPORT); + } + } + } + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/SideEffect.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/SideEffect.java new file mode 100644 index 0000000..e3874b8 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/beings/SideEffect.java @@ -0,0 +1,38 @@ +package com.simiyutin.au.roguelike.models.beings; + +import java.awt.event.KeyEvent; +import java.util.function.Function; + + +/** + * Entity that maps user input to one of predefined profiles. + */ +public enum SideEffect { + + IDENTITY(Function.identity()), + + INVERSED(key -> { + switch (key) { + case KeyEvent.VK_S: + return KeyEvent.VK_W; + case KeyEvent.VK_W: + return KeyEvent.VK_S; + case KeyEvent.VK_A: + return KeyEvent.VK_D; + case KeyEvent.VK_D: + return KeyEvent.VK_A; + default: + return key; + } + }); + + private Function proxy; + + SideEffect(Function proxy) { + this.proxy = proxy; + } + + public Integer apply(Integer key) { + return proxy.apply(key); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Item.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Item.java new file mode 100644 index 0000000..ef0601c --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Item.java @@ -0,0 +1,15 @@ +package com.simiyutin.au.roguelike.models.items; + +import java.awt.*; + + +/** + * Some object that can be useful for player. + */ +public interface Item { + Color getColor(); + + String getName(); + + char getGlyph(); +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/MedAid.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/MedAid.java new file mode 100644 index 0000000..07a5134 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/MedAid.java @@ -0,0 +1,34 @@ +package com.simiyutin.au.roguelike.models.items; + +import java.awt.*; + + +/** + * Health pack. + */ +public class MedAid implements Item { + + private int value = 50; + + @Override + public Color getColor() { + return Color.GREEN; + } + + @Override + public String getName() { + return String.format("Med aid of value %d", value); + } + + @Override + public char getGlyph() { + return '+'; + } + + /** + * @return how much health player will recover after picking this medAid up. + */ + public int getValue() { + return value; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/ThrownItem.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/ThrownItem.java new file mode 100644 index 0000000..c23a083 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/ThrownItem.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.roguelike.models.items; + +import com.simiyutin.au.roguelike.models.Position; +import com.simiyutin.au.roguelike.models.World; + + +/** + * Wrapper around Item that holds position on the map. + */ +public class ThrownItem { + public int x; + public int y; + private Item item; + + public ThrownItem(Item item, World world) { + this.item = item; + + Position position = world.getEmptyPosition(); + this.x = position.x; + this.y = position.y; + } + + public Item getItem() { + return item; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Weapon.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Weapon.java new file mode 100644 index 0000000..2047869 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/Weapon.java @@ -0,0 +1,70 @@ +package com.simiyutin.au.roguelike.models.items; + +import java.awt.*; + + +/** + * Weapon itself. + */ +public class Weapon implements Item { + private final int MAX_LEVEL = 10; + private WeaponType type; + private int level; // 1 - 10 + + public Weapon(WeaponType type, int level) { + this.type = type; + this.level = level; + } + + /** + * Get weapon of random type of given level. + */ + public static Weapon getRandomOfLevel(int level) { + return new Weapon(WeaponType.getRandom(), level); + } + + public Color getColor() { + int brightness = 150 + level / MAX_LEVEL * 100; + return Color.getHSBColor(type.getHue(), 200, brightness); + } + + public String getName() { // todo handle level name correlation + if (type == WeaponType.HAND) return type.getName(); + return type.getName() + String.format(" of level %d", level); + } + + /** + * @return measure of harm this weapon can damage. + */ + public int getHarm() { + return type.getHarm(); + } + + public char getGlyph() { + return type.getGlyph(); + } + + public int getLevel() { + return level; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Weapon weapon = (Weapon) o; + + if (MAX_LEVEL != weapon.MAX_LEVEL) return false; + if (level != weapon.level) return false; + return type == weapon.type; + } + + @Override + public int hashCode() { + int result = type != null ? type.hashCode() : 0; + result = 31 * result + MAX_LEVEL; + result = 31 * result + level; + return result; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/WeaponType.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/WeaponType.java new file mode 100644 index 0000000..1caa7e9 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/models/items/WeaponType.java @@ -0,0 +1,55 @@ +package com.simiyutin.au.roguelike.models.items; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + + +/** + * Specifies glyph, color and initial damage of weapon of given type. + */ +public enum WeaponType { + HAND('h', "hand", 1, 10), + SWORD('>', "sword", 10, 150); + + private char glyph; + private int hue; + private int harm; + private String name; + + WeaponType(char glyph, String name, int harm, int hue) { + this.glyph = glyph; + this.name = name; + this.harm = harm; + this.hue = hue; + } + + /** + * @return random weapon except "HAND" + */ + public static WeaponType getRandom() { + List types = Arrays.asList(SWORD); + Integer ind = new Random().nextInt(types.size()); + return types.get(ind); + } + + public String getName() { + return name; + } + + public char getGlyph() { + return glyph; + } + + public int getHarm() { + return harm; + } + + public int getHue() { + return hue; + } + + public Weapon getItem() { + return new Weapon(this, 1); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/DeadScreen.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/DeadScreen.java new file mode 100644 index 0000000..af3857d --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/DeadScreen.java @@ -0,0 +1,41 @@ +package com.simiyutin.au.roguelike.screens; + +import asciiPanel.AsciiPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.event.KeyEvent; + + +/** + * "You are dead. Retry?" screen + */ +public class DeadScreen implements Screen { + private static final Logger LOGGER = LogManager.getLogger(DeadScreen.class); + + public DeadScreen() { + LOGGER.trace("player died"); + } + + + @Override + public void display(AsciiPanel terminal) { + print(terminal, "You are dead. Retry? [enter]"); + } + + @Override + public Screen respondToUserInput(KeyEvent key) { + switch (key.getKeyCode()) { + case KeyEvent.VK_ENTER: + LOGGER.trace("player pressed retry button"); + return new PlayScreen(); + default: + return this; + } + } + + @Override + public Screen updateState() { + return this; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/PlayScreen.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/PlayScreen.java new file mode 100644 index 0000000..567cf5c --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/PlayScreen.java @@ -0,0 +1,134 @@ +package com.simiyutin.au.roguelike.screens; + +import asciiPanel.AsciiPanel; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.Being; +import com.simiyutin.au.roguelike.models.beings.Player; +import com.simiyutin.au.roguelike.models.items.Item; +import com.simiyutin.au.roguelike.models.items.ThrownItem; +import com.simiyutin.au.roguelike.util.WorldFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.awt.event.KeyEvent; + + +/** + * Main screen where all the business takes place. + */ +public class PlayScreen implements Screen { + + private static final Logger LOGGER = LogManager.getLogger(PlayScreen.class); + private final World world; + private final int screenWidth = 80; + private final int screenHeight = 24; + + + public PlayScreen() { + world = WorldFactory.getDefaultConfigurationOfMinLevel(1); + LOGGER.trace("Game screen created"); + } + + @Override + public void display(AsciiPanel terminal) { + int left = scrollLeft(); + int top = scrollTop(); + displayWorld(terminal, left, top); + displayMobs(terminal, left, top); + displayLoot(terminal, left, top); + displayInfo(terminal); + displayMessage(terminal); + } + + @Override + public Screen respondToUserInput(KeyEvent key) { + Player player = world.getPlayer(); + switch (player.getEffect().apply(key.getKeyCode())) { + case KeyEvent.VK_S: + player.move(0, 1); + break; + case KeyEvent.VK_W: + player.move(0, -1); + break; + case KeyEvent.VK_A: + player.move(-1, 0); + break; + case KeyEvent.VK_D: + player.move(1, 0); + break; + case KeyEvent.VK_ENTER: + player.act(); + break; + case KeyEvent.VK_P: + world.printTiles(); + break; + case KeyEvent.VK_Z: + world.getPlayer().levelUp(); + break; + case KeyEvent.VK_I: + world.getPlayer().changeWeapon(); + break; + } + return updateState(); + } + + @Override + public Screen updateState() { + return world.getPlayer().getHealth() > 0 ? this : new DeadScreen(); + } + + private void displayMobs(AsciiPanel terminal, int left, int top) { + + for (Being b: world.getMobs()) { + writeSafe(terminal, b.getGlyph(), b.x - left, b.y - top, b.getColor()); + } + + Player p = world.getPlayer(); + writeSafe(terminal, p.getGlyph(), p.x - left, p.y - top, p.getColor()); + + } + + private void displayWorld(AsciiPanel terminal, int left, int top) { + + for (int x = 0; x < screenWidth; x++) { + for (int y = 0; y < screenHeight; y++) { + int wx = x + left; + int wy = y + top; + + terminal.write(world.getGlyph(wx, wy), x, y, world.getColor(wx, wy)); + } + } + } + + private void displayLoot(AsciiPanel terminal, int left, int top) { + for (ThrownItem b : world.getThrownItems()) { + Item item = b.getItem(); + writeSafe(terminal, item.getGlyph(), b.x - left, b.y - top, item.getColor()); + } + } + + private void displayMessage(AsciiPanel terminal) { + terminal.write(world.getMessage(), 40 - world.getMessage().length() / 2, 5); + } + + private void displayInfo(AsciiPanel terminal) { + terminal.write(String.format("level: %s", world.getPlayer().getLevel()), 1, 1); + terminal.write(String.format("health: %s", world.getPlayer().getHealth()), 1, 2); + terminal.write(String.format("weapon: %s", world.getPlayer().getWeapon().getName()), 1, 3); + } + + private int scrollTop() { + return Math.max(0, Math.min(world.getPlayer().y - screenHeight / 2, world.getHeight() - screenHeight)); + } + + private int scrollLeft() { + return Math.max(0, Math.min(world.getPlayer().x - screenWidth / 2, world.getWidth() - screenWidth)); + } + + private void writeSafe(AsciiPanel terminal, char c, int x, int y, Color color) { + if (x > 0 && x < screenWidth && y > 0 && y < screenHeight) { + terminal.write(c, x, y, color); + } + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/Screen.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/Screen.java new file mode 100644 index 0000000..82823d9 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/Screen.java @@ -0,0 +1,35 @@ +package com.simiyutin.au.roguelike.screens; + +import asciiPanel.AsciiPanel; + +import java.awt.event.KeyEvent; + + +/** + * Representation of the game is split into screens. + * Each of them has its purpose, can be drawn, and reacts to user input. + */ +public interface Screen { + /** + * print screen representation to terminal + */ + void display(AsciiPanel terminal); + + /** + * Change state according to user input and return self. + */ + Screen respondToUserInput(KeyEvent key); + + /** + * Change state according to time passed and return self. + */ + Screen updateState(); + + /** + * Print message to terminal + */ + default void print(AsciiPanel terminal, String str) { + final int onHeight = 18; + terminal.writeCenter(str, onHeight); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/StartScreen.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/StartScreen.java new file mode 100644 index 0000000..7406ff0 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/screens/StartScreen.java @@ -0,0 +1,35 @@ +package com.simiyutin.au.roguelike.screens; + +import asciiPanel.AsciiPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.event.KeyEvent; + + +/** + * "Press [enter] to start" screen. + */ +public class StartScreen implements Screen { + + private static final Logger LOGGER = LogManager.getLogger(StartScreen.class); + + public StartScreen() { + LOGGER.trace("start screen showed"); + } + + @Override + public void display(AsciiPanel terminal) { + print(terminal, "Press [enter] to start"); + } + + @Override + public Screen respondToUserInput(KeyEvent key) { + return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this; + } + + @Override + public Screen updateState() { + return this; + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/DelayedTask.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/DelayedTask.java new file mode 100644 index 0000000..a6f0d4d --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/DelayedTask.java @@ -0,0 +1,21 @@ +package com.simiyutin.au.roguelike.util; + +import java.util.Timer; +import java.util.TimerTask; + + +/** + * Runnable with delayed start. + */ +public class DelayedTask { + + public DelayedTask(Runnable task, long timeout) { + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + task.run(); + } + }, timeout); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RandomBGColorGenerator.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RandomBGColorGenerator.java new file mode 100644 index 0000000..a5fd9ab --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RandomBGColorGenerator.java @@ -0,0 +1,20 @@ +package com.simiyutin.au.roguelike.util; + +import java.awt.*; +import java.util.Random; + + +/** + * Generates color from preset color range. + */ +public class RandomBGColorGenerator { + private final Random randomGen = new Random(42); + + public Color getColor() { + return Color.getHSBColor(getComponent(), 0.8f, 0.4f); + } + + private float getComponent() { + return (float) randomGen.nextDouble(); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RecurringTask.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RecurringTask.java new file mode 100644 index 0000000..646e7ec --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/RecurringTask.java @@ -0,0 +1,20 @@ +package com.simiyutin.au.roguelike.util; + +import java.util.TimerTask; + + +/** + * Runnable with period of repetition. + */ +public class RecurringTask { + + public RecurringTask(Runnable task, long period) { + java.util.Timer timer = new java.util.Timer(true); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + task.run(); + } + }, 0, period); + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldBuilder.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldBuilder.java new file mode 100644 index 0000000..08b1064 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldBuilder.java @@ -0,0 +1,171 @@ +package com.simiyutin.au.roguelike.util; + +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.Being; +import com.simiyutin.au.roguelike.models.items.Item; +import com.simiyutin.au.roguelike.models.items.MedAid; +import com.simiyutin.au.roguelike.models.items.ThrownItem; +import com.simiyutin.au.roguelike.models.items.Weapon; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + + +/** + * Builds world. (!) + */ +public class WorldBuilder { + + private static final RandomBGColorGenerator colorGen = new RandomBGColorGenerator(); + private static final Logger LOGGER = LogManager.getLogger(WorldBuilder.class); + private int width; + private int height; + private Tile[][] tiles; + private List> mobs; + private List loot; + private int minLevel; + + public WorldBuilder(int width, int height) { + this.width = width; + this.height = height; + + this.tiles = new Tile[width][height]; + fillWithFloor(); + + this.mobs = new ArrayList<>(); + this.loot = new ArrayList<>(); + this.minLevel = 1; + + + } + + public World build() { + World world = new World(tiles); + world.setMinLevel(minLevel); + Color color = colorGen.getColor(); + Tile.FLOOR.setColor(color); + Tile.WALL.setColor(color); + + for (Constructor c : mobs) { + try { + world.getMobs().add(c.newInstance(world)); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + LOGGER.error(e); + } + } + + for (Item w : loot) { + world.getThrownItems().add(new ThrownItem(w, world)); //todo circular dependency + } + + world.setMinLevel(minLevel); + + return world; + } + + /** + * Add random caves to world. + */ + public WorldBuilder withCaves() { + randomizeTiles(); + smooth(8); + return this; + } + + /** + * Set minimum level of mobs and items. + */ + public WorldBuilder ofMinLevel(int level) { + this.minLevel = level; + return this; + } + + /** + * Add mobs of given class and given quantity. + */ + public WorldBuilder addMobs(Class clazz, int quantity) { + for (int i = 0; i < quantity; i++) { + try { + mobs.add(clazz.getConstructor(World.class)); + } catch (NoSuchMethodException ex) { + LOGGER.error(ex); + } + } + return this; + } + + /** + * Add random weapons of given quantity. + */ + public WorldBuilder addWeapons(int quantity) { + Random randGen = new Random(); + for (int i = 0; i < quantity; i++) { + Weapon weapon = Weapon.getRandomOfLevel(minLevel + randGen.nextInt(2)); + loot.add(weapon); + } + return this; + } + + /** + * Add medAids of given quantity. + */ + public WorldBuilder addMedAids(int quantity) { + for (int i = 0; i < quantity; i++) { + MedAid medAid = new MedAid(); + loot.add(medAid); + } + + return this; + } + + private void randomizeTiles() { + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + tiles[x][y] = Math.random() < 0.5 ? Tile.FLOOR : Tile.WALL; + } + } + } + + private void smooth(int times) { + Tile[][] tiles2 = new Tile[width][height]; + for (int time = 0; time < times; time++) { + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int floors = 0; + int rocks = 0; + + for (int ox = -1; ox < 2; ox++) { + for (int oy = -1; oy < 2; oy++) { + if (x + ox < 0 || x + ox >= width || y + oy < 0 + || y + oy >= height) + continue; + + if (tiles[x + ox][y + oy] == Tile.FLOOR) + floors++; + else + rocks++; + } + } + tiles2[x][y] = floors >= rocks ? Tile.FLOOR : Tile.WALL; + } + } + tiles = tiles2; + } + } + + private void fillWithFloor() { + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + tiles[i][j] = Tile.FLOOR; + } + } + } +} diff --git a/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldFactory.java b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldFactory.java new file mode 100644 index 0000000..ae861c5 --- /dev/null +++ b/roguelike/impl/src/main/java/com/simiyutin/au/roguelike/util/WorldFactory.java @@ -0,0 +1,31 @@ +package com.simiyutin.au.roguelike.util; + +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.Dragon; +import com.simiyutin.au.roguelike.models.beings.Ghost; +import com.simiyutin.au.roguelike.models.beings.Mushroom; + + +/** + * Default World configurations. + */ +public class WorldFactory { + + + /** + * Default configuration used in game. + */ + public static World getDefaultConfigurationOfMinLevel(int level) { + World result = new WorldBuilder(100, 100) + .withCaves() + .ofMinLevel(level) + .addMobs(Mushroom.class, 10) + .addMobs(Ghost.class, 10) + .addMobs(Dragon.class, 5) + .addWeapons(5) + .addMedAids(10) + .build(); + + return result; + } +} diff --git a/roguelike/impl/src/main/resources/log4j2.xml b/roguelike/impl/src/main/resources/log4j2.xml new file mode 100644 index 0000000..58b0db9 --- /dev/null +++ b/roguelike/impl/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/roguelike/impl/src/main/resources/todolist b/roguelike/impl/src/main/resources/todolist new file mode 100644 index 0000000..6a9ccf6 --- /dev/null +++ b/roguelike/impl/src/main/resources/todolist @@ -0,0 +1,56 @@ +--1. Текст сообщения о том, что происходит + +--2. AI +--3. Призрак - гоняется за нами, если догоняет, то инвертирует наше управление. + После этого, все призраки от нас убегают, и чтобы вернуть управление назад, нам надо догнать. + +--4. Оружие, аптечки: + --Дешевое оружие и аптечки раскиданы по карте, более дорогое оружие можно получить только у дракона. + --При взятии нового оружия, старое пропадает + --Интенсивностью отмечаем уровень оружия + --Отображать текущее оружие рядом со здоровьем + +--5. --Дракон - когда наступаем на него, начинается бой, если побеждаем, то увеличиваем свой уровень . + --Рядом с драконом лежит лут, но когда мы его забираем, то начинается драка, и нельзя уйти. + --Драка такая - надо много раз нажать заданную клавишу. + +--6. Урон от дракона и грибов зависит от их уровня + Следовательно, надо будет разработать формулу для подсчета этого урона. + +--7. Переход на новые локации: + --Есть телепорты, переходя на них, генерируется новая карта, с другим цветом + --(более сложная, то есть все существа там более высокого уровня) + --Телепорт появляется только после убивания всех драконов + +8. Спрайты - держим буфер фильтров, которые наслоятся на следующие кадры анимации. + Когда отрисовываем кадр, считываем фильтр из буфера и отрисовываем поверх + +9. Туториал, в виде текста на отделном экране и по нажатию кнопки + + +ОБЯЗАТЕЛЬНЫЕ ЗАДАНИЯ: +--1. Юнит - тесты +--2. Логирование (например, исключений) + + +БАГИ: +--1. Не работает в русской раскладке +--2. В reversed состоянии не работает копание +--3. После победы над драконом можно поднять меч более низкого уровня +4. ConcurrentModificationException. хорошо воспроизводится на тесте testEmptyPosition + + + + + + + + + + + + + + + + diff --git a/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/MobsTests.java b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/MobsTests.java new file mode 100644 index 0000000..d758fa4 --- /dev/null +++ b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/MobsTests.java @@ -0,0 +1,176 @@ +package com.simiyutin.au.roguelike; + +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.*; +import com.simiyutin.au.roguelike.models.items.MedAid; +import com.simiyutin.au.roguelike.models.items.ThrownItem; +import com.simiyutin.au.roguelike.models.items.Weapon; +import com.simiyutin.au.roguelike.models.items.WeaponType; +import com.simiyutin.au.roguelike.util.WorldBuilder; +import org.junit.Test; + +import static java.lang.Thread.sleep; +import static junit.framework.TestCase.*; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Created by boris on 13.05.17. + */ +public class MobsTests { + + private World world; + private Player player; + + @Test + public void testAlive() { + init(); + assertTrue(player.isAlive()); + } + + @Test + public void testCanMove() { + init(); + world.setTile(50, 51, Tile.WALL); + assertFalse(player.canMove(50, 51)); + } + + @Test + public void testMove() { + init(); + int prevX = player.x; + player.move(1, 0); + assertThat(player.x, is(prevX + 1)); + } + + @Test + public void testMoveToPoisonedFloor() { + init(); + world.setTile(50, 51, Tile.POISONED_FLOOR); + int healthBefore = player.getHealth(); + player.move(0, 1); + assertTrue(player.getHealth() < healthBefore); + } + + @Test + public void testMoveOverEdge() { + init(); + player.x = 0; + player.y = 0; + player.move(-1, 0); + assertThat(player.x, is(0)); + } + + @Test + public void testMoveImmobilized() { + init(); + player.setImmobilized(true); + int prevX = player.x; + player.move(1, 0); + assertThat(player.x, is(prevX)); + + } + + @Test + public void testAct() { + init(); + world.setTile(50, 51, Tile.WALL); + player.move(0, 1); + player.act(); + assertThat(world.getTile(50, 51), is(Tile.FLOOR)); + } + + @Test + public void testGhost() { + init(); + Ghost.isSelfActing = false; + Ghost ghost = new Ghost(world); + world.getMobs().add(ghost); + ghost.x = 50 + Ghost.TRIGGER_RANGE; + ghost.y = 50; + ghost.interactWithEnvironment(); + assertThat(player.getEffect(), is(SideEffect.IDENTITY)); + ghost.x--; + ghost.interactWithEnvironment(); + assertThat(player.getEffect(), is(SideEffect.INVERSED)); + + ghost.interactWithEnvironment(); + assertThat(player.getEffect(), is(SideEffect.IDENTITY)); + assertTrue(world.getMobs().isEmpty()); + assertFalse(ghost.isAlive()); + } + + @Test + public void testDragon() throws InterruptedException { + init(); + Dragon dragon = new Dragon(world); + world.getMobs().add(dragon); + dragon.x = 50; + dragon.y = 51; + + int dragonHealthBefore = dragon.getHealth(); + int playerHealthBefore = player.getHealth(); + + player.act(); + sleep(1000); + player.act(); + + assertTrue(player.getHealth() < playerHealthBefore); + assertTrue(dragon.getHealth() < dragonHealthBefore); + + } + + @Test + public void testMushroom() { + init(); + Mushroom mushroom = new Mushroom(world); + int healthBefore = player.getHealth(); + player.x = mushroom.x; + player.y = mushroom.y + 1; + assertThat(world.getTile(player.x, player.y), is(Tile.POISONED_FLOOR)); + player.move(1, 0); + assertTrue(player.getHealth() < healthBefore); + } + + @Test + public void testPickUpWeapon() { + init(); + ThrownItem sword = new ThrownItem(new Weapon(WeaponType.SWORD, 1), world); + world.getThrownItems().add(sword); + player.x = sword.x; + player.y = sword.y; + + player.interactWithEnvironment(); + player.changeWeapon(); + assertEquals(sword.getItem(), player.getWeapon()); + assertTrue(world.getThrownItems().isEmpty()); + } + + @Test + public void testPickUpMed() { + init(); + ThrownItem medAidPos = new ThrownItem(new MedAid(), world); + MedAid medAid = ((MedAid) medAidPos.getItem()); + + world.getThrownItems().add(medAidPos); + player.x = medAidPos.x; + player.y = medAidPos.y; + int prevHealth = player.getHealth(); + player.setHealth(player.getHealth() - medAid.getValue() - 10); + player.interactWithEnvironment(); + + assertThat(player.getHealth(), is(prevHealth - 10)); + assertTrue(world.getThrownItems().isEmpty()); + } + + + private void init() { + world = new WorldBuilder(100, 100) + .build(); + + player = world.getPlayer(); + player.x = 50; + player.y = 50; + } +} diff --git a/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldBuilderTests.java b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldBuilderTests.java new file mode 100644 index 0000000..34dfd65 --- /dev/null +++ b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldBuilderTests.java @@ -0,0 +1,101 @@ +package com.simiyutin.au.roguelike; + +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.*; +import com.simiyutin.au.roguelike.models.items.Item; +import com.simiyutin.au.roguelike.models.items.MedAid; +import com.simiyutin.au.roguelike.models.items.ThrownItem; +import com.simiyutin.au.roguelike.models.items.Weapon; +import com.simiyutin.au.roguelike.util.WorldBuilder; +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +public class WorldBuilderTests { + + @Test + public void testSimple() { + World world = new WorldBuilder(100, 100).build(); + assertThat(world.getHeight(), is(100)); + assertThat(world.getWidth(), is(100)); + assertTrue(world.getThrownItems().isEmpty()); + assertTrue(world.getMobs().isEmpty()); + assertTrue(world.getPlayer().isAlive()); + assertThat(world.getPlayer().getWeapon(), CoreMatchers.notNullValue()); + } + + + + @Test + public void testMobs() { + World world = new WorldBuilder(100, 100) + .withCaves() + .addMobs(Mushroom.class, 10) + .addMobs(Ghost.class, 10) + .addMobs(Dragon.class, 10) + .build(); + + assertThat(world.getMobs().stream().filter(m -> m instanceof Mushroom).count(), is(10L)); + assertThat(world.getMobs().stream().filter(m -> m instanceof Ghost).count(), is(10L)); + assertThat(world.getMobs().stream().filter(m -> m instanceof Dragon).count(), is(10L)); + } + + @Test + public void testMinLevel() { + World world = new WorldBuilder(100, 100) + .withCaves() + .ofMinLevel(10) + .addMobs(Mushroom.class, 10) + .addMobs(Ghost.class, 10) + .addMobs(Dragon.class, 10) + .addWeapons(10) + .build(); + + assertThat(world.getMinLevel(), is(10)); + + for (Being mob : world.getMobs()) { + if (mob instanceof ActiveBeing) { + assertTrue(((ActiveBeing) mob).getLevel() >= world.getMinLevel()); + } + } + + for (ThrownItem item : world.getThrownItems()) { + if (item.getItem() instanceof Weapon) { + Weapon w = ((Weapon) item.getItem()); + assertTrue(w.getLevel() >= world.getMinLevel()); + } + } + } + + + @Test + public void testWeapons() { + World world = new WorldBuilder(100, 100) + .withCaves() + .addWeapons(10) + .build(); + + assertThat(world.getThrownItems().stream().map(ThrownItem::getItem).filter(i -> i instanceof Weapon).count(), is(10L)); + + for (Item item : world.getThrownItems().stream().map(ThrownItem::getItem).filter(i -> i instanceof Weapon).collect(Collectors.toList())) { + Weapon weapon = (Weapon) item; + assertTrue(weapon.getLevel() >= world.getMinLevel()); + } + } + + @Test + public void testMedAids() { + World world = new WorldBuilder(100, 100) + .withCaves() + .addWeapons(10) + .addMedAids(10) + .build(); + + assertThat(world.getThrownItems().stream().map(ThrownItem::getItem).filter(i -> i instanceof MedAid).count(), is(10L)); + } +} diff --git a/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldTests.java b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldTests.java new file mode 100644 index 0000000..344cbe2 --- /dev/null +++ b/roguelike/impl/src/test/java/com/simiyutin/au/roguelike/WorldTests.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.roguelike; + +import com.simiyutin.au.roguelike.models.Position; +import com.simiyutin.au.roguelike.models.Tile; +import com.simiyutin.au.roguelike.models.World; +import com.simiyutin.au.roguelike.models.beings.Ghost; +import com.simiyutin.au.roguelike.util.WorldBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Created by boris on 13.05.17. + */ +public class WorldTests { + + @Test + public void testEmptyPosition() { + World world = new WorldBuilder(100, 100) + .withCaves() + .addMedAids(50) + .addWeapons(50) + .addMobs(Ghost.class, 100) + .build(); + + for (int i = 0; i < 100000; i++) { + Position position = world.getEmptyPosition(); + assertTrue(world.getMob(position.x, position.y) == null); + assertTrue(world.getItem(position.x, position.y) == null); + assertTrue(world.getTile(position.x, position.y) == Tile.FLOOR); + } + } +} diff --git a/shell/.gradle/2.10/taskArtifacts/cache.properties b/shell/.gradle/2.10/taskArtifacts/cache.properties deleted file mode 100644 index 34e0db3..0000000 --- a/shell/.gradle/2.10/taskArtifacts/cache.properties +++ /dev/null @@ -1 +0,0 @@ -#Sat Mar 04 19:36:05 MSK 2017 diff --git a/shell/.gradle/2.10/taskArtifacts/cache.properties.lock b/shell/.gradle/2.10/taskArtifacts/cache.properties.lock deleted file mode 100644 index 7a2565a..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/cache.properties.lock and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/fileHashes.bin b/shell/.gradle/2.10/taskArtifacts/fileHashes.bin deleted file mode 100644 index 4374ba0..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/fileHashes.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin b/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin deleted file mode 100644 index b6a62f2..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/fileSnapshots.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin b/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin deleted file mode 100644 index 9cea602..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/outputFileStates.bin and /dev/null differ diff --git a/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin b/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin deleted file mode 100644 index 04bc858..0000000 Binary files a/shell/.gradle/2.10/taskArtifacts/taskArtifacts.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/cache.properties b/shell/.gradle/3.1/taskArtifacts/cache.properties deleted file mode 100644 index 2f07c7a..0000000 --- a/shell/.gradle/3.1/taskArtifacts/cache.properties +++ /dev/null @@ -1 +0,0 @@ -#Tue Feb 28 22:43:07 MSK 2017 diff --git a/shell/.gradle/3.1/taskArtifacts/cache.properties.lock b/shell/.gradle/3.1/taskArtifacts/cache.properties.lock deleted file mode 100644 index fb71597..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/cache.properties.lock and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/fileHashes.bin b/shell/.gradle/3.1/taskArtifacts/fileHashes.bin deleted file mode 100644 index 6d05f69..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/fileHashes.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin b/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin deleted file mode 100644 index 6963709..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/fileSnapshots.bin and /dev/null differ diff --git a/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin b/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin deleted file mode 100644 index 395bf7c..0000000 Binary files a/shell/.gradle/3.1/taskArtifacts/taskArtifacts.bin and /dev/null differ diff --git a/shell/build.gradle b/shell/build.gradle index afbf593..0db6fb4 100644 --- a/shell/build.gradle +++ b/shell/build.gradle @@ -2,6 +2,8 @@ group 'simiyutin' version '1.0-SNAPSHOT' apply plugin: 'java' +apply plugin: 'application' +mainClassName = "com.simiyutin.au.shell.core.Main" sourceCompatibility = 1.8 @@ -9,6 +11,14 @@ repositories { mavenCentral() } +run{ + standardInput = System.in +} + dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' + + // https://mvnrepository.com/artifact/commons-cli/commons-cli + compile group: 'commons-cli', name: 'commons-cli', version: '1.2' + } diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java new file mode 100644 index 0000000..6198516 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Cat.java @@ -0,0 +1,54 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.IOException; +import java.nio.charset.MalformedInputException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Scanner; + +/** + * Reads file from parameter to stdout or reflects stdin to stdout + */ +public class Cat extends Command { + + public static final String NAME = "cat"; + + public Cat(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + + if (stream.hasNext()) { + return stream; + } + + Stream res = new Stream(); + if (args.isEmpty()) { + Scanner sc = new Scanner(System.in); + while (true) { + System.out.println(sc.nextLine()); + } + } else { + String filename = args.get(0); + Path file = Paths.get(filename); + try { + List lines = Files.readAllLines(file); + lines.forEach(res::write); + } catch (MalformedInputException e) { + throw new CommandExecutionException("Cannot determine file encoding"); + } catch (IOException e) { + throw new CommandExecutionException("Error while reading file: " + e.toString()); + } + } + return res; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java new file mode 100644 index 0000000..80487d5 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Echo.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; +import java.util.stream.Collectors; + + +/** + * Prints arguments to output stream + */ +public class Echo extends Command { + + public static final String NAME = "echo"; + + public Echo(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + + String toPrint = args.stream().collect(Collectors.joining()); + + Stream stream = new Stream(); + stream.write(toPrint); + return stream; + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java new file mode 100644 index 0000000..e088b7d --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Eq.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; + +/** + * Puts new variable in context or changes variable value + */ +public class Eq extends Command { + + public Eq(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + String var = args.get(0); + String val = args.get(1); + env.put(var, val); + return new Stream(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java new file mode 100644 index 0000000..8a9d4e9 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Exit.java @@ -0,0 +1,26 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.util.List; + +/** + * Exits shell + */ +public class Exit extends Command { + + public static final String NAME = "exit"; + + public Exit(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + System.exit(0); + return null; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Grep.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Grep.java new file mode 100644 index 0000000..015eaf0 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Grep.java @@ -0,0 +1,119 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import org.apache.commons.cli.*; + +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Searches in input stream or file using regular expressions. + */ +public class Grep extends Command { + + public static final String NAME = "grep"; + + private boolean isInsensitive = false; + private boolean isFullWord = false; + private int linesAfterMatch = 0; + + public Grep(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + + Options options = new Options(); + options.addOption("i", false, "case insensitivity"); + options.addOption("w", false, "full word search"); + options.addOption("A", true, "number of lines to append after match"); + CommandLine cmd = parseArgs(options); + + isInsensitive = cmd.hasOption('i'); + isFullWord = cmd.hasOption('w'); + try { + String a = cmd.getOptionValue('A'); + linesAfterMatch = a == null ? 0 : Integer.valueOf(a); + return grep(stream, cmd.getArgs()); + } catch (NumberFormatException e) { + throw new CommandExecutionException("grep: incorrect '-A' value, required number"); + } + } + + private Stream grep(Stream stream, String[] args) throws CommandExecutionException { + + if (args.length == 0) { + throw new CommandExecutionException("grep: pattern must be provided"); + } else if (args.length > 1) { + String pattern = args[0]; + String fileName = args[1]; + Stream fileStream = new Cat(Collections.singletonList(fileName), new Environment()).run(new Stream()); + return grepFromStream(pattern, fileStream); + } else { + String pattern = args[0]; + return grepFromStream(pattern, stream); + } + } + + private Stream grepFromStream(String regex, Stream input) { + + Stream output = new Stream(); + + if (isFullWord) { + regex = String.format("\\b%s\\b", regex); + } + + if (isInsensitive) { + regex = "(?i)" + regex; + } + + Pattern pattern = Pattern.compile("(" + regex + ")"); + + while (input.hasNext()) { + String line = input.read(); + Matcher m = pattern.matcher(line); + if (m.find()) { + String replaced = m.replaceAll(colorize("$1")); + output.write(replaced); + appendN(input, output, linesAfterMatch); + } + } + + return output; + } + + private void appendN(Stream input, Stream output, int n) { + for (int i = 0; i < n; i++) { + if (input.hasNext()) { + output.write(input.read()); + } else { + break; + } + } + } + + private String colorize(String string) { + final int esc = 27; + final String csi = (char) esc + "["; + final String redModifier = "31m"; + final String resetModifier = "0m"; + return csi + redModifier + string + csi + resetModifier; + } + + private CommandLine parseArgs(Options options) throws CommandExecutionException { + CommandLineParser parser = new PosixParser(); + try { + String[] arr = new String[0]; + CommandLine cmd = parser.parse(options, args.toArray(arr)); + return cmd; + } catch (ParseException e) { + throw new CommandExecutionException("grep: wrong arguments"); + } + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java b/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java new file mode 100644 index 0000000..2e369ef --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/OutSource.java @@ -0,0 +1,59 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.*; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Passes input arguments and stream to external shell and reads result from its stdout + */ +public class OutSource extends Command { + + private String commandName; + + public OutSource(String commandName, List args, Environment env) { + super(args, env); + this.commandName = commandName; + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + String command = commandName + " " + args.stream().collect(Collectors.joining(" ")); + Stream output = new Stream(); + try { + Process process = Runtime.getRuntime().exec(command); + write(process, stream); + output = read(process); + } catch (IOException | InterruptedException e) { + throw new CommandExecutionException("System error: " + e.toString()); + } + return output; + } + + private void write(Process process, Stream stream) throws IOException { + Writer handle = new PrintWriter(process.getOutputStream()); + final int lineFeed = 10; + while (stream.hasNext()) { + handle.write(stream.read()); + handle.write(lineFeed); + } + handle.close(); + } + + private Stream read(Process process) throws IOException, InterruptedException { + Stream output = new Stream(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + output.write(line); + } + process.waitFor(); + return output; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java new file mode 100644 index 0000000..d802de4 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Pwd.java @@ -0,0 +1,31 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Prints current directory + */ +public class Pwd extends Command { + + public static final String NAME = "pwd"; + + public Pwd(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream ignored) throws CommandExecutionException { + Path currentRelativePath = Paths.get(""); + String currentDir = currentRelativePath.toAbsolutePath().toString(); + Stream stream = new Stream(); + stream.write(currentDir); + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java b/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java new file mode 100644 index 0000000..f2e9222 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/commands/Wc.java @@ -0,0 +1,88 @@ +package com.simiyutin.au.shell.commands; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import com.simiyutin.au.shell.core.Command; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Stream; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * Counts lines, words and bytes in given file or input stream + */ +public class Wc extends Command { + + public static final String NAME = "wc"; + + public Wc(List args, Environment env) { + super(args, env); + } + + @Override + public Stream run(Stream stream) throws CommandExecutionException { + if (!args.isEmpty()) { + return handleFile(); + } else if (stream.hasNext()) { + return handleInputStream(stream); + } else { + handleStdIn(); + return new Stream(); + } + } + + private Stream handleFile() { + String fileName = args.get(0); + Path file = Paths.get(fileName); + try { + List lines = Files.readAllLines(file); + return parseLines(lines); + } catch (IOException e) { + e.printStackTrace(); + return new Stream(); + } + } + + private Stream handleInputStream(Stream stream) { + List lines = new ArrayList<>(); + while (stream.hasNext()) { + lines.add(stream.read()); + } + return parseLines(lines); + } + + private void handleStdIn() { + Scanner sc = new Scanner(System.in); + while (true) { + List lines = Collections.singletonList(sc.nextLine()); + Stream result = parseLines(lines); + System.out.println(result.read()); + } + } + + private Stream parseLines(List lines) { + int linesCount = lines.size(); + char[] concatenated = lines.stream().collect(Collectors.joining(" ")).toCharArray(); + long wordCount = 0; + for (char c : concatenated) { + if (c == ' ') { + wordCount++; + } + } + wordCount++; + + long byteCount = concatenated.length; + + String result = String.format(" %d %d %d", linesCount, wordCount, byteCount); + Stream stream = new Stream(); + stream.write(result); + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Command.java b/shell/src/main/java/com/simiyutin/au/shell/core/Command.java new file mode 100644 index 0000000..76190ac --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Command.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; + +import java.util.List; + +/** + * Abstract class which impersonates Command entity. + * Command can be run with Stream as an argument and can modify environment of a shell. + */ +public abstract class Command { + + protected List args; + protected Environment env; + + + /** + * Public constructor + * @param args arguments of a command as a List + * @param env environment of current running shell + */ + public Command(List args, Environment env) { + this.args = args; + this.env = env; + } + + /** + * Used as interface to command object. Takes input stream and returns result as output stream. + * @param stream input data as a Stream object + * @return output data as a Stream object + */ + public abstract Stream run(Stream stream) throws CommandExecutionException; +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java b/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java new file mode 100644 index 0000000..d6d7d73 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/CommandExecutor.java @@ -0,0 +1,33 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; + +import java.util.List; + + +/** + * Entity to implement chained computations. + * Every computation is processed one after another + * and result of current one is passed to the next one. + */ +public class CommandExecutor { + private CommandExecutor() {} + + /** + * Sequentially executes chained list of commands. + * @param commands List of Command objects + * @return Stream object which contain result of a chain of computations + */ + public static Stream run(List commands) { + Stream stream = new Stream(); + try { + for (Command command : commands) { + stream = command.run(stream); + } + } catch (CommandExecutionException e) { + System.out.println(e.toString()); + return new Stream(); + } + return stream; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java b/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java new file mode 100644 index 0000000..64049c6 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/CommandFactory.java @@ -0,0 +1,64 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.commands.*; + +import java.util.List; + + +/** + * Factory which is used to construct Command objects relying on command token + */ +public class CommandFactory { + + + /** + * Factory method to generate commands from tokens. + * @param commandToken token, which contains command name as a String + * @param args arguments of a command + * @param env Environment object + * @return generated runnable Command object + */ + public static Command produce(Token commandToken, List args, Environment env) { + Command command; + switch (commandToken.getType()) { + case EQ: + command = new Eq(args, env); + return command; + case WORD: + command = getCommand(commandToken.toString(), args, env); + return command; + default: + return null; + } + } + + private static Command getCommand(String commandName, List args, Environment env) { + + Command command = null; + switch (commandName) { + case Echo.NAME: + command = new Echo(args, env); + break; + case Cat.NAME: + command = new Cat(args, env); + break; + case Wc.NAME: + command = new Wc(args, env); + break; + case Pwd.NAME: + command = new Pwd(args, env); + break; + case Grep.NAME: + command = new Grep(args, env); + break; + case Exit.NAME: + command = new Exit(args, env); + break; + default: + command = new OutSource(commandName, args, env); + break; + + } + return command; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java b/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java new file mode 100644 index 0000000..37dcc9c --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Environment.java @@ -0,0 +1,42 @@ +package com.simiyutin.au.shell.core; + +import java.util.HashMap; +import java.util.Map; + +/** + * Entity which represents a concept of environment which handles state of a running shell. + * It can be asked to give variable by its name and to accept new variable by name and value + */ +public class Environment { + private Map map; + + public Environment() { + this.map = new HashMap<>(); + } + + /** + * Get value of variable + * @param var name of requested variable + * @return value of a requested variable or empty string if such does not exist in environment + */ + public String get(String var) { + + String res = map.get(var); + + if (res == null) { + return ""; + } else { + return res; + } + } + + + /** + * Put variable value + * @param var name of inserted variable + * @param val value of inserted variable + */ + public void put(String var, String val) { + map.put(var, val); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Main.java b/shell/src/main/java/com/simiyutin/au/shell/core/Main.java new file mode 100644 index 0000000..b22e91e --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Main.java @@ -0,0 +1,8 @@ +package com.simiyutin.au.shell.core; + +public class Main { + public static void main(String[] args) { + Shell shell = new Shell(); + shell.run(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java b/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java new file mode 100644 index 0000000..1adc5ef --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Parser.java @@ -0,0 +1,129 @@ +package com.simiyutin.au.shell.core; + +import com.simiyutin.au.shell.core.exceptions.ParserException; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * A DFA which accepts a list of tokens and evaluates + * corresponding shell command or a sequence of them + */ +public class Parser { + + private Queue queue; + private Environment env; + private List commands = new ArrayList<>(); + + private Parser(List tokens, Environment env) { + this.queue = new ArrayDeque<>(tokens); + this.env = env; + } + + + /** + * Parses list of tokens into list of commands + * @param tokens List of tokens to parse as a chain of shell commands + * @param env Environment object + * @return List of generated commands + */ + public static List run(List tokens, Environment env) { + try { + Parser parser = new Parser(tokens, env); + parser.start(); + return parser.commands; + } catch (ParserException e) { + System.out.println(e.toString()); + return new ArrayList<>(); + } + } + + private void start() throws ParserException { + Token token = queue.poll(); + if (token.getType() == Token.Type.WORD) { + parseCommand(token); + } else { + error("Expression must start with a command"); + } + + } + + private void parseCommand(Token firstWord) throws ParserException { + Token token = queue.poll(); + switch (token.getType()) { + case EQ: + parseEQ(firstWord); + break; + case WORD: + List args = new ArrayList<>(); + args.add(token.toString()); + parseArg(firstWord, args); + break; + case PIPE: + createCommand(firstWord, new ArrayList<>()); + start(); + break; + case EOF: + createCommand(firstWord, new ArrayList<>()); + break; + default: + error("Unexpected token"); + break; + } + } + + private void createCommand(Token commandToken, List args) throws ParserException { + Command command = CommandFactory.produce(commandToken, args, env); + commands.add(command); + } + + private void parseEQ(Token var) throws ParserException { + Token val = queue.poll(); + switch (val.getType()) { + case WORD: + checkEQSyntax(); + List args = new ArrayList<>(); + args.add(var.toString()); + args.add(val.toString()); + createCommand(Token.eq(), args); + break; + default: + error("Violated syntax of assignment operator"); + break; + } + } + + private void checkEQSyntax() throws ParserException { + Token token = queue.poll(); + if (token.getType() != Token.Type.EOF) { + error("Violated syntax of assignment operator"); + } + } + + private void parseArg(Token command, List args) throws ParserException { + Token token = queue.poll(); + switch (token.getType()) { + case WORD: + args.add(token.toString()); + parseArg(command, args); + break; + case PIPE: + createCommand(command, args); + start(); + break; + case EOF: + createCommand(command, args); + break; + default: + error("Violated syntax of argument list"); + break; + } + } + + private void error(String what) throws ParserException { + throw new ParserException(what); + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java b/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java new file mode 100644 index 0000000..154e4d1 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Preprocessor.java @@ -0,0 +1,65 @@ +package com.simiyutin.au.shell.core; + +/** + * Takes care of substitution variable values in requested places + */ +public class Preprocessor { + + /** + * Substitutes variables in unprocessed string + * @param unprocessed unprocessed input String + * @param env Environment object + * @return String with substituted variables from environment + */ + public static String run(String unprocessed, Environment env) { + + char[] chars = unprocessed.toCharArray(); + + StringBuilder varName = new StringBuilder(); + StringBuilder processed = new StringBuilder(); + boolean varReading = false; + boolean isInsideDoubleQuotes = false; + boolean isInsideSingleQuotes = false; + + for (int i = 0; i < chars.length; i++) { + + if (!isInsideSingleQuotes && chars[i] == '$') { + varReading = true; + continue; + } + + if (varReading) { + varName.append(chars[i]); + if (isEndOfVarName(chars, i)) { + processed.append(env.get(varName.toString())); + varName.delete(0, chars.length); + varReading = false; + } + continue; + } + + if (chars[i] == '"' && !isInsideSingleQuotes) { + isInsideDoubleQuotes = !isInsideDoubleQuotes; + } + + if (chars[i] == '\'' && !isInsideDoubleQuotes) { + isInsideSingleQuotes = !isInsideSingleQuotes; + } + + processed.append(chars[i]); + } + + return processed.toString(); + } + + private static boolean isEndOfVarName(char[] chars, int index) { + + if (index == chars.length - 1) { + return true; + } + + char c = chars[index + 1]; + return c == ' ' || c == '"' || c == '\'' || c == '$'; + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java b/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java new file mode 100644 index 0000000..dab43d4 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Shell.java @@ -0,0 +1,38 @@ +package com.simiyutin.au.shell.core; + +import java.util.List; +import java.util.Scanner; + + +/** + * Main entity. Reads from stdin in an infinite loop and executes commands + */ +public class Shell { + + private Environment env; + + /** + * constructs Shell with empty environment + */ + public Shell() { + env = new Environment(); + } + + /** + * runs Shell + */ + public void run() { + Scanner sc = new Scanner(System.in); + + while (true) { + String input = sc.nextLine(); + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream stream = CommandExecutor.run(commands); + while (stream.hasNext()) { + System.out.println(stream.read()); + } + } + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java b/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java new file mode 100644 index 0000000..2873004 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Stream.java @@ -0,0 +1,59 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * Entity to handle input and output of commands. + */ +public class Stream { + + private Queue vals; + + public Stream() { + this.vals = new ArrayDeque<>(); + } + + /** + * Read one line from stream + * @return next line in stream or null if stream is empty + */ + public String read() { + return vals.poll(); + } + + /** + * Write one line to stream + * @param val line to add to stream + */ + public void write(String val) { + vals.offer(val); + } + + /** + * Check if stream has more lines to read + * @return true if stream has at least one line to read + */ + public boolean hasNext() { + return vals.peek() != null; + } + + /** + * String representation of a stream object + * @return concatenated lines in stream + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Queue replacement = new ArrayDeque<>(); + while (hasNext()) { + String line = read(); + sb.append('\n'); + sb.append(line); + replacement.add(line); + } + vals = replacement; + sb.delete(0, 1); + return sb.toString(); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Token.java b/shell/src/main/java/com/simiyutin/au/shell/core/Token.java new file mode 100644 index 0000000..70defcd --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Token.java @@ -0,0 +1,162 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayList; + +/** + * Describes possible lexemes + */ +public class Token { + + /** + * describes possible token types + */ + public enum Type { + SINGLE_QUOTE, + DOUBLE_QUOTE, + WHITESPACE, + PIPE, + WORD, + EQ, + EOF + } + + private String value; + private Type type; + + private static final String SINGLE_QUOTE = "'"; + private static final String DOUBLE_QUOTE = "\""; + private static final String EQUALITY_OPERATOR = "="; + private static final String WHITESPACE = " "; + private static final String PIPE = "|"; + private static final String EOF = "\0"; + + private Token(Token.Type type, String value) { + this.type = type; + this.value = value; + } + + /** + * Static factory method + * @return new single quote token + */ + public static Token singleQuote() { + return new Token(Type.SINGLE_QUOTE, SINGLE_QUOTE); + } + + /** + * Static factory method + * @return new double quote token + */ + public static Token doubleQuote() { + return new Token(Type.DOUBLE_QUOTE, DOUBLE_QUOTE); + } + + /** + * Static factory method + * @return new '=' token + */ + public static Token eq() { + return new Token(Type.EQ, EQUALITY_OPERATOR); + } + + /** + * Static factory method + * @return new whitespace token + */ + public static Token whitespace() { + return new Token(Type.WHITESPACE, WHITESPACE); + } + + /** + * Static factory method + * @return new pipe token + */ + public static Token pipe() { + return new Token(Type.PIPE, PIPE); + } + + /** + * Static factory method + * @param word string to encapsulate in newly created token + * @return new word token + */ + public static Token word(String word) { + return new Token(Type.WORD, word); + } + + /** + * Static factory method + * @return new end of line token + */ + public static Token eof() { + return new Token(Type.EOF, EOF); + } + + + /** + * Tests if given char corresponds to any delimiter token + * @param c character to test + */ + public static boolean isDelimiter(char c) { + ArrayList test = new ArrayList<>(); + test.add(SINGLE_QUOTE); + test.add(DOUBLE_QUOTE); + test.add(WHITESPACE); + test.add(EQUALITY_OPERATOR); + test.add(PIPE); + return test.contains(String.valueOf(c)); + } + + /** + * Static factory method + * @param c character to get corresponding token + * @return generate corresponding token to passed char + */ + public static Token valueOf(char c) { + switch (String.valueOf(c)) { + case SINGLE_QUOTE: + return singleQuote(); + case DOUBLE_QUOTE: + return doubleQuote(); + case WHITESPACE: + return whitespace(); + case EQUALITY_OPERATOR: + return eq(); + case PIPE: + return pipe(); + default: + return word(String.valueOf(c)); + } + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Token token = (Token) o; + + if (value != null ? !value.equals(token.value) : token.value != null) return false; + return type == token.type; + } + + @Override + public int hashCode() { + int result = value != null ? value.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java b/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java new file mode 100644 index 0000000..d902e0f --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/Tokenizer.java @@ -0,0 +1,74 @@ +package com.simiyutin.au.shell.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/** + * Takes care of translating given char string to list of lexemes. + */ +public class Tokenizer { + + /** + * Start tokenizer on preprocessed string + * @param input processed string corresponding to some shell command + * @return list of words and operators obtained from passed string + */ + public static List run(String input) { + + List tokens = new ArrayList<>(); + + char[] chars = input.toCharArray(); + for (int i = 0; i < chars.length;) { + char c = chars[i]; + if (!Token.isDelimiter(c)) { + String word = readWord(chars, i); + i += word.length(); + tokens.add(Token.word(word)); + continue; + } + Token.Type type = Token.valueOf(c).getType(); + switch (type) { + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + i++; + String word = readWordInsideQuotes(chars, i, c); + i += word.length() + 1; + tokens.add(Token.word(word)); + break; + case EQ: + tokens.add(Token.eq()); + i++; + break; + case WHITESPACE: + i++; + break; + case PIPE: + tokens.add(Token.pipe()); + i++; + break; + } + } + + tokens.add(Token.eof()); + + return tokens; + } + + private static String readWordInsideQuotes(char[] chars, int i, char matcher) { + return readWordUntil(c -> c.equals(matcher), chars, i); + } + + private static String readWord(char[] chars, int i) { + return readWordUntil(Token::isDelimiter, chars, i); + } + + private static String readWordUntil(Predicate breaker, char[] chars, int i) { + StringBuilder sb = new StringBuilder(); + while (i < chars.length && !breaker.test(chars[i])) { + sb.append(chars[i++]); + } + return sb.toString(); + } + +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java new file mode 100644 index 0000000..b34eda5 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/CommandExecutionException.java @@ -0,0 +1,10 @@ +package com.simiyutin.au.shell.core.exceptions; + +/** + * This is used when errors while commands execution encountered + */ +public class CommandExecutionException extends ShellException { + public CommandExecutionException(String what) { + super(what); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java new file mode 100644 index 0000000..c7d68a1 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ParserException.java @@ -0,0 +1,11 @@ +package com.simiyutin.au.shell.core.exceptions; + + +/** + * This is thrown when command has incorrect syntax + */ +public class ParserException extends ShellException { + public ParserException(String what) { + super(what); + } +} diff --git a/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java new file mode 100644 index 0000000..77003e3 --- /dev/null +++ b/shell/src/main/java/com/simiyutin/au/shell/core/exceptions/ShellException.java @@ -0,0 +1,17 @@ +package com.simiyutin.au.shell.core.exceptions; + + +/** + * Base class for shell exceptions + */ +public class ShellException extends Exception { + private String what = ""; + public ShellException(String what) { + this.what = what; + } + + @Override + public String toString() { + return what; + } +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/GrepTests.java b/shell/src/test/java/com/simiyutin/au/shell/GrepTests.java new file mode 100644 index 0000000..9e1a8d4 --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/GrepTests.java @@ -0,0 +1,136 @@ +package com.simiyutin.au.shell; + +import com.simiyutin.au.shell.core.*; +import com.simiyutin.au.shell.core.exceptions.CommandExecutionException; +import org.junit.Test; + +import java.util.List; + +import static junit.framework.TestCase.assertEquals; + +public class GrepTests { + + @Test + public void smokeTest() { + + String input = "echo 'hello world' | grep hello"; + String expected = colorize("hello") + " world"; + testTemplate(input, expected); + + } + + @Test + public void noMatchTest() { + + String input = "echo 'hello world' | grep noMatch"; + String expected = ""; + testTemplate(input, expected); + + } + + @Test + public void insensitivityTest() { + + String input = "echo 'hello world' | grep -i HELLO"; + String expected = colorize("hello") + " world"; + testTemplate(input, expected); + + } + + @Test + public void wholeWordTest() { + + String input = "echo 'hello hell.' | grep -w hell"; + String expected = "hello " + colorize("hell") + "."; + testTemplate(input, expected); + + } + + @Test + public void linesAfterTest() throws CommandExecutionException { + + Stream input = new Stream(); + input.write("hello"); + input.write("1"); + input.write("2"); + input.write("3"); + input.write("4"); + input.write("5"); + input.write("6"); + input.write("7"); + input.write("8"); + input.write("9"); + + String command = "grep -A 3 hello"; + + List tokens = Tokenizer.run(command); + List commands = Parser.run(tokens, new Environment()); + Stream output = commands.get(0).run(input); + + Stream expected = new Stream(); + expected.write(colorize("hello")); + expected.write("1"); + expected.write("2"); + expected.write("3"); + + assertEquals(expected.toString(), output.toString()); + + } + + @Test + public void overflowLinesTest() throws CommandExecutionException { + + Stream input = new Stream(); + input.write("hello"); + input.write("1"); + input.write("2"); + input.write("3"); + + String command = "grep -A 10 hello"; + + List tokens = Tokenizer.run(command); + List commands = Parser.run(tokens, new Environment()); + Stream output = commands.get(0).run(input); + + Stream expected = new Stream(); + expected.write(colorize("hello")); + expected.write("1"); + expected.write("2"); + expected.write("3"); + + assertEquals(expected.toString(), output.toString()); + + } + + @Test + public void multipleMatchTest() { + + String input = "echo 'hello hell' | grep hell"; + String expected = String.format("%so %s", colorize("hell"), colorize("hell")); + testTemplate(input, expected); + + } + + + private Stream interpret(String input) { + Environment env = new Environment(); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + return output; + } + + private void testTemplate(String input, String expected) { + Stream output = interpret(input); + String actual = output.toString(); + assertEquals(expected, actual); + } + + private String colorize(String string) { + final int esc = 27; + final String csi = (char) esc + "["; + final String redModifier = "31m"; + final String resetModifier = "0m"; + return csi + redModifier + string + csi + resetModifier; + } +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java b/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java new file mode 100644 index 0000000..4429e8e --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/ParserTests.java @@ -0,0 +1,92 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.*; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ParserTests { + + @Test + public void smokeTest() throws Exception { + Environment env = getEnv(); + String input = "hello=hacked"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked", Preprocessor.run("$hello", env)); + } + + @Test + public void reduceTest() throws Exception { + Environment env = getEnv(); + String input = "hello='hacked = | azazazazaa'"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked = | azazazazaa", Preprocessor.run("$hello", env)); + } + + @Test + public void reduceWithSubstitutionTest() throws Exception { + Environment env = getEnv(); + String input = "hello=\"hacked $vladimir\""; + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked putin", Preprocessor.run("$hello", env)); + } + + @Test + public void recursiveReduceWithSubstitutionTest() throws Exception { + Environment env = getEnv(); + String input = "hello=\"hacked $hello\""; + input = Preprocessor.run(input, env); + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + commands.get(0).run(new Stream()); + assertEquals("hacked world", Preprocessor.run("$hello", env)); + } + + @Test + public void echoTest() { + Environment env = getEnv(); + String input = "echo hello"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("hello", output.toString()); + } + + @Test + public void echoToPipeToCatTest() { + Environment env = getEnv(); + String input = "echo hello | cat"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("hello", output.toString()); + } + + @Test + public void echoToSedAsOutSourceTest() { + + Environment env = getEnv(); + String input = "echo \"hello_world\" | sed 's/hello/goodbye/'"; + List tokens = Tokenizer.run(input); + List commands = Parser.run(tokens, env); + Stream output = CommandExecutor.run(commands); + assertEquals("goodbye_world", output.toString()); + } + + private Environment getEnv() { + Environment env = new Environment(); + env.put("hello", "world"); + env.put("vladimir", "putin"); + + return env; + } +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java b/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java new file mode 100644 index 0000000..38a3f52 --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/PreprocessorTests.java @@ -0,0 +1,73 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.Environment; +import com.simiyutin.au.shell.core.Preprocessor; + +import static org.junit.Assert.assertEquals; + +public class PreprocessorTests { + + @Test + public void smokeTest() { + + Environment env = getEnv(); + + assertEquals("cat world | echo putin azazaza", + Preprocessor.run("cat $hello | echo $vladimir azazaza", env)); + } + + @Test + public void emptySubstitutionTest() { + + Environment env = getEnv(); + + assertEquals("", Preprocessor.run("$not_in_env", env)); + } + + @Test + public void lastSub() { + Environment env = getEnv(); + + assertEquals("world", Preprocessor.run("$hello", env)); + } + + @Test + public void varNameBreakers() { + Environment env = getEnv(); + + assertEquals("world'hidden' \"world\" worldputin putin", + Preprocessor.run("$hello'hidden' \"$hello\" $hello$vladimir $vladimir", env)); + } + + @Test + public void hidesInSingleQuotes() { + Environment env = getEnv(); + + assertEquals("'$hello' world", Preprocessor.run("'$hello' $hello", env)); + } + + @Test + public void handlesIncorrectInput() { + Environment env = getEnv(); + + assertEquals("'$hello", Preprocessor.run("'$hello", env)); + } + + @Test + public void nestedQuotes() { + Environment env = getEnv(); + + assertEquals("\"'hello'\" world", Preprocessor.run("\"'$test'\" world", env)); + } + + private Environment getEnv() { + Environment env = new Environment(); + env.put("hello", "world"); + env.put("vladimir", "putin"); + env.put("test", "hello"); + + return env; + } + +} diff --git a/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java b/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java new file mode 100644 index 0000000..f524f10 --- /dev/null +++ b/shell/src/test/java/com/simiyutin/au/shell/TokenizerTests.java @@ -0,0 +1,67 @@ +package com.simiyutin.au.shell; + +import org.junit.Test; +import com.simiyutin.au.shell.core.Token; +import com.simiyutin.au.shell.core.Tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class TokenizerTests { + + @Test + public void smokeTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("hello world")); + expected.add(Token.eof()); + + List actual = Tokenizer.run("'hello world'"); + + assertEquals(expected, actual); + } + + @Test + public void emptyTest() { + List expected = new ArrayList<>(); + expected.add(Token.eof()); + + List actual = Tokenizer.run(""); + + assertEquals(expected, actual); + } + + @Test + public void assignmentTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("hello")); + expected.add(Token.eq()); + expected.add(Token.word("world")); + expected.add(Token.eof()); + + List actual = Tokenizer.run("hello=world"); + + assertEquals(expected, actual); + } + + @Test + public void mixedTest() { + List expected = new ArrayList<>(); + expected.add(Token.word("cat")); + expected.add(Token.word("input.txt")); + expected.add(Token.pipe()); + expected.add(Token.word("cat")); + expected.add(Token.pipe()); + expected.add(Token.word("grep")); + expected.add(Token.word("-v")); + expected.add(Token.word("azazaazz")); + expected.add(Token.word("long \"string\" constant")); + expected.add(Token.eof()); + + + List actual = Tokenizer.run("cat input.txt | cat | grep -v azazaazz 'long \"string\" constant' "); + + assertEquals(expected, actual); + } +}