2018-02-28 23:51:40 +01:00
package inf101.v18.rogue101.game ;
import java.util.ArrayList ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Map ;
import java.util.Random ;
import java.util.function.Supplier ;
import inf101.v18.gfx.Screen ;
import inf101.v18.gfx.gfxmode.ITurtle ;
import inf101.v18.gfx.gfxmode.TurtlePainter ;
import inf101.v18.gfx.textmode.Printer ;
import inf101.v18.grid.GridDirection ;
import inf101.v18.grid.IGrid ;
import inf101.v18.grid.ILocation ;
import inf101.v18.rogue101.Main ;
2018-03-09 23:18:00 +01:00
import inf101.v18.rogue101.enemies.Girl ;
2018-02-28 23:51:40 +01:00
import inf101.v18.rogue101.examples.Carrot ;
2018-03-14 00:00:52 +01:00
import inf101.v18.rogue101.items.* ;
2018-02-28 23:51:40 +01:00
import inf101.v18.rogue101.examples.Rabbit ;
import inf101.v18.rogue101.map.GameMap ;
import inf101.v18.rogue101.map.IGameMap ;
import inf101.v18.rogue101.map.IMapView ;
import inf101.v18.rogue101.map.MapReader ;
2018-03-07 21:40:06 +01:00
import inf101.v18.rogue101.objects.* ;
2018-02-28 23:51:40 +01:00
import javafx.scene.canvas.GraphicsContext ;
import javafx.scene.input.KeyCode ;
import javafx.scene.paint.Color ;
public class Game implements IGame {
/ * *
* All the IActors that have things left to do this turn
* /
private List < IActor > actors = Collections . synchronizedList ( new ArrayList < > ( ) ) ;
/ * *
* For fancy solution to factory problem
* /
private Map < String , Supplier < IItem > > itemFactories = new HashMap < > ( ) ;
/ * *
* Useful random generator
* /
private Random random = new Random ( ) ;
2018-03-10 15:17:50 +01:00
/ * *
* Saves the last three messages
* /
private List < String > lastMessages = new ArrayList < > ( ) ;
2018-02-28 23:51:40 +01:00
/ * *
* The game map . { @link IGameMap } gives us a few more details than
* { @link IMapView } ( write access to item lists ) ; the game needs this but
* individual items don ' t .
* /
private IGameMap map ;
private IActor currentActor ;
private ILocation currentLocation ;
private int movePoints = 0 ;
private final ITurtle painter ;
private final Printer printer ;
private int numPlayers = 0 ;
2018-03-13 21:35:38 +01:00
public Game ( Screen screen , ITurtle painter , Printer printer ) {
2018-02-28 23:51:40 +01:00
this . painter = painter ;
this . printer = printer ;
2018-03-07 21:40:06 +01:00
addFactory ( ) ;
2018-02-28 23:51:40 +01:00
// NOTE: in a more realistic situation, we will have multiple levels (one map
// per level), and (at least for a Roguelike game) the levels should be
// generated
//
// inputGrid will be filled with single-character strings indicating what (if
// anything)
// should be placed at that map square
2018-03-10 15:17:50 +01:00
IGrid < String > inputGrid = MapReader . readFile ( " maps/testmap.txt " ) ;
2018-02-28 23:51:40 +01:00
if ( inputGrid = = null ) {
System . err . println ( " Map not found – falling back to builtin map " ) ;
inputGrid = MapReader . readString ( Main . BUILTIN_MAP ) ;
}
this . map = new GameMap ( inputGrid . getArea ( ) ) ;
for ( ILocation loc : inputGrid . locations ( ) ) {
IItem item = createItem ( inputGrid . get ( loc ) ) ;
if ( item ! = null ) {
map . add ( loc , item ) ;
}
}
2018-03-14 00:00:52 +01:00
// Prints some helpful information.
String [ ] info = { " Controls: " , " WASD or arrow keys for movement " , " E to pick up an item " , " Q to drop an item " , " 1-5 to choose an item " , " N to change name " , " ENTER to confirm " , " R to use a ranged attack " , " F to use a magic attack " } ;
for ( int i = 0 ; i < info . length ; i + + ) {
this . printer . printAt ( map . getWidth ( ) + 2 , 1 + i , info [ i ] ) ;
}
2018-02-28 23:51:40 +01:00
}
public Game ( String mapString ) {
2018-03-08 00:57:53 +01:00
printer = new Printer ( 1280 , 720 ) ;
2018-03-07 22:34:18 +01:00
painter = new TurtlePainter ( 1280 , 720 ) ;
2018-03-07 21:40:06 +01:00
addFactory ( ) ;
2018-02-28 23:51:40 +01:00
IGrid < String > inputGrid = MapReader . readString ( mapString ) ;
this . map = new GameMap ( inputGrid . getArea ( ) ) ;
for ( ILocation loc : inputGrid . locations ( ) ) {
IItem item = createItem ( inputGrid . get ( loc ) ) ;
if ( item ! = null ) {
map . add ( loc , item ) ;
}
}
}
@Override
public void addItem ( IItem item ) {
map . add ( currentLocation , item ) ;
}
@Override
public void addItem ( String sym ) {
IItem item = createItem ( sym ) ;
if ( item ! = null )
map . add ( currentLocation , item ) ;
}
2018-03-13 21:35:38 +01:00
/ * *
* Calculates the attack of an IActor based on equipped items .
*
* @return The attack
* /
private int getAttack ( ) {
int damage = currentActor . getAttack ( ) + random . nextInt ( 20 ) + 1 ;
IWeapon weapon = ( IWeapon ) currentActor . getItem ( IWeapon . class ) ;
if ( weapon ! = null ) {
damage + = weapon . getWeaponDamage ( ) ;
}
IBuffItem buff = ( IBuffItem ) currentActor . getItem ( IBuffItem . class ) ;
if ( buff ! = null ) {
damage + = buff . getBuffDamage ( ) ;
}
return damage ;
}
/ * *
* Gets the defence of the current target .
*
* @param target The target to evaluate
* @return The defence of the target
* /
private int getDefence ( IItem target ) {
int defence = target . getDefence ( ) + 10 ;
IActor actor = ( IActor ) target ;
IBuffItem item = ( IBuffItem ) actor . getItem ( IBuffItem . class ) ;
if ( item ! = null ) {
defence + = item . getBuffDefence ( ) ;
}
return defence ;
}
/ * *
* Gets the damage done against the current target .
*
* @param target The target to evaluate .
* @return The damage done to the target .
* /
private int getDamage ( IItem target ) {
int damage = currentActor . getDamage ( ) ;
IActor actor = ( IActor ) target ;
IBuffItem item = ( IBuffItem ) actor . getItem ( IBuffItem . class ) ;
if ( item ! = null ) {
damage - = item . getBuffDamageReduction ( ) ;
}
return damage ;
}
2018-02-28 23:51:40 +01:00
@Override
public ILocation attack ( GridDirection dir , IItem target ) {
2018-03-08 23:38:59 +01:00
ILocation loc = currentLocation . go ( dir ) ;
if ( ! map . has ( loc , target ) ) {
2018-02-28 23:51:40 +01:00
throw new IllegalMoveException ( " Target isn't there! " ) ;
2018-03-08 23:38:59 +01:00
}
2018-03-13 21:35:38 +01:00
if ( getAttack ( ) > = getDefence ( target ) ) {
int actualDamage = target . handleDamage ( this , target , getDamage ( target ) ) ;
2018-03-08 23:38:59 +01:00
formatMessage ( " %s hits %s for %d damage " , currentActor . getName ( ) , target . getName ( ) , actualDamage ) ;
} else {
2018-03-13 21:35:38 +01:00
formatMessage ( " %s tried to hit %s, but missed " , currentActor . getName ( ) , target . getName ( ) ) ;
2018-03-08 23:38:59 +01:00
}
2018-02-28 23:51:40 +01:00
map . clean ( loc ) ;
if ( target . isDestroyed ( ) ) {
return move ( dir ) ;
} else {
movePoints - - ;
return currentLocation ;
}
}
2018-03-13 21:35:38 +01:00
@Override
public ILocation rangedAttack ( GridDirection dir , IItem target ) {
ILocation loc = currentLocation ;
if ( getAttack ( ) > = getDefence ( target ) ) {
int damage = getDamage ( target ) / loc . gridDistanceTo ( map . getLocation ( target ) ) ;
int actualDamage = target . handleDamage ( this , target , damage ) ;
formatMessage ( " %s hits %s for %d damage " , currentActor . getName ( ) , target . getName ( ) , actualDamage ) ;
} else {
formatMessage ( " %s tried to hit %s, but missed " , currentActor . getName ( ) , target . getName ( ) ) ;
}
map . clean ( map . getLocation ( target ) ) ;
if ( target . isDestroyed ( ) & & map . has ( currentLocation . go ( dir ) , target ) ) {
return move ( dir ) ;
} else {
return currentLocation ;
}
}
2018-02-28 23:51:40 +01:00
/ * *
* Begin a new game turn , or continue to the previous turn
*
* @return True if the game should wait for more user input
* /
public boolean doTurn ( ) {
do {
if ( actors . isEmpty ( ) ) {
// System.err.println("new turn!");
// no one in the queue, we're starting a new turn!
// first collect all the actors:
beginTurn ( ) ;
}
2018-03-01 21:13:25 +01:00
if ( random . nextInt ( 100 ) < 20 ) {
2018-03-07 21:40:06 +01:00
ILocation loc = map . getLocation ( random . nextInt ( map . getWidth ( ) ) , random . nextInt ( map . getHeight ( ) ) ) ;
2018-03-01 21:13:25 +01:00
if ( ! map . hasActors ( loc ) & & ! map . hasItems ( loc ) & & ! map . hasWall ( loc ) ) {
map . add ( loc , new Carrot ( ) ) ;
}
}
2018-02-28 23:51:40 +01:00
// process actors one by one; for the IPlayer, we return and wait for keypresses
2018-03-10 15:17:50 +01:00
// Possible for INonPlayer, we could also return early (returning
2018-02-28 23:51:40 +01:00
// *false*), and then insert a little timer delay between each non-player move
// (the timer
// is already set up in Main)
while ( ! actors . isEmpty ( ) ) {
// get the next player or non-player in the queue
currentActor = actors . remove ( 0 ) ;
if ( currentActor . isDestroyed ( ) ) // skip if it's dead
continue ;
currentLocation = map . getLocation ( currentActor ) ;
if ( currentLocation = = null ) {
displayDebug ( " doTurn(): Whoops! Actor has disappeared from the map: " + currentActor ) ;
}
movePoints = 1 ; // everyone gets to do one thing
if ( currentActor instanceof INonPlayer ) {
// computer-controlled players do their stuff right away
( ( INonPlayer ) currentActor ) . doTurn ( this ) ;
// remove any dead items from current location
map . clean ( currentLocation ) ;
2018-03-10 15:17:50 +01:00
return false ;
2018-02-28 23:51:40 +01:00
} else if ( currentActor instanceof IPlayer ) {
if ( currentActor . isDestroyed ( ) ) {
// a dead human player gets removed from the game
// TODO: you might want to be more clever here
displayMessage ( " YOU DIE!!! " ) ;
map . remove ( currentLocation , currentActor ) ;
currentActor = null ;
currentLocation = null ;
} else {
// For the human player, we need to wait for input, so we just return.
// Further keypresses will cause keyPressed() to be called, and once the human
// makes a move, it'll lose its movement point and doTurn() will be called again
//
// NOTE: currentActor and currentLocation are set to the IPlayer (above),
// so the game remembers who the player is whenever new keypresses occur. This
// is also how e.g., getLocalItems() work – the game always keeps track of
// whose turn it is.
return true ;
}
} else {
displayDebug ( " doTurn(): Hmm, this is a very strange actor: " + currentActor ) ;
}
}
} while ( numPlayers > 0 ) ; // we can safely repeat if we have players, since we'll return (and break out of
// the loop) once we hit the player
return true ;
}
/ * *
* Go through the map and collect all the actors .
* /
private void beginTurn ( ) {
numPlayers = 0 ;
// this extra fancy iteration over each map location runs *in parallel* on
// multicore systems!
// that makes some things more tricky, hence the "synchronized" block and
// "Collections.synchronizedList()" in the initialization of "actors".
// NOTE: If you want to modify this yourself, it might be a good idea to replace
// "parallelStream()" by "stream()", because weird things can happen when many
// things happen
// at the same time! (or do INF214 or DAT103 to learn about locks and threading)
map . getArea ( ) . parallelStream ( ) . forEach ( ( loc ) - > { // will do this for each location in map
List < IItem > list = map . getAllModifiable ( loc ) ; // all items at loc
Iterator < IItem > li = list . iterator ( ) ; // manual iterator lets us remove() items
while ( li . hasNext ( ) ) { // this is what "for(IItem item : list)" looks like on the inside
IItem item = li . next ( ) ;
if ( item . getCurrentHealth ( ) < 0 ) {
// normally, we expect these things to be removed when they are destroyed, so
// this shouldn't happen
synchronized ( this ) {
formatDebug ( " beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n " ,
item . getName ( ) , item . getSymbol ( ) , loc ) ;
}
li . remove ( ) ;
map . remove ( loc , item ) ; // need to do this too, to update item map
} else if ( item instanceof IPlayer ) {
actors . add ( 0 , ( IActor ) item ) ; // we let the human player go first
synchronized ( this ) {
numPlayers + + ;
}
} else if ( item instanceof IActor ) {
actors . add ( ( IActor ) item ) ; // add other actors to the end of the list
2018-03-01 21:13:25 +01:00
} else if ( item instanceof Carrot ) {
( ( Carrot ) item ) . doTurn ( ) ;
2018-02-28 23:51:40 +01:00
}
}
} ) ;
}
@Override
public boolean canGo ( GridDirection dir ) {
return map . canGo ( currentLocation , dir ) ;
}
2018-03-07 21:40:06 +01:00
private void addFactory ( ) {
itemFactories . put ( " # " , Wall : : new ) ;
itemFactories . put ( " @ " , Player : : new ) ;
itemFactories . put ( " C " , Carrot : : new ) ;
itemFactories . put ( " R " , Rabbit : : new ) ;
itemFactories . put ( " M " , Manga : : new ) ;
2018-03-09 23:18:00 +01:00
itemFactories . put ( " G " , Girl : : new ) ;
2018-03-07 21:40:06 +01:00
itemFactories . put ( " . " , Dust : : new ) ;
2018-03-12 21:26:36 +01:00
itemFactories . put ( " S " , Sword : : new ) ;
2018-03-14 00:00:52 +01:00
itemFactories . put ( " c " , Chest : : new ) ;
2018-03-07 21:40:06 +01:00
}
2018-02-28 23:51:40 +01:00
@Override
public IItem createItem ( String sym ) {
switch ( sym ) {
case " " :
return null ;
default :
// alternative/advanced method
Supplier < IItem > factory = itemFactories . get ( sym ) ;
if ( factory ! = null ) {
return factory . get ( ) ;
} else {
System . err . println ( " createItem: Don't know how to create a ' " + sym + " ' " ) ;
return null ;
}
}
}
@Override
public void displayDebug ( String s ) {
printer . clearLine ( Main . LINE_DEBUG ) ;
printer . printAt ( 1 , Main . LINE_DEBUG , s , Color . DARKRED ) ;
System . err . println ( s ) ;
}
@Override
public void displayMessage ( String s ) {
2018-03-10 15:17:50 +01:00
if ( lastMessages . size ( ) > = 3 ) {
lastMessages . remove ( 2 ) ;
}
lastMessages . add ( 0 , s ) ;
2018-02-28 23:51:40 +01:00
printer . clearLine ( Main . LINE_MSG1 ) ;
2018-03-10 15:17:50 +01:00
printer . clearLine ( Main . LINE_MSG2 ) ;
printer . clearLine ( Main . LINE_MSG3 ) ;
if ( lastMessages . size ( ) > 0 ) {
printer . printAt ( 1 , Main . LINE_MSG1 , lastMessages . get ( 0 ) ) ;
}
if ( lastMessages . size ( ) > 1 ) {
printer . printAt ( 1 , Main . LINE_MSG2 , lastMessages . get ( 1 ) ) ;
}
if ( lastMessages . size ( ) > 2 ) {
printer . printAt ( 1 , Main . LINE_MSG3 , lastMessages . get ( 2 ) ) ;
}
2018-02-28 23:51:40 +01:00
System . out . println ( " Message: « " + s + " » " ) ;
}
@Override
public void displayStatus ( String s ) {
printer . clearLine ( Main . LINE_STATUS ) ;
printer . printAt ( 1 , Main . LINE_STATUS , s ) ;
System . out . println ( " Status: « " + s + " » " ) ;
}
public void draw ( ) {
map . draw ( painter , printer ) ;
}
@Override
public boolean drop ( IItem item ) {
if ( item ! = null ) {
map . add ( currentLocation , item ) ;
return true ;
} else
return false ;
}
@Override
public void formatDebug ( String s , Object . . . args ) {
displayDebug ( String . format ( s , args ) ) ;
}
@Override
public void formatMessage ( String s , Object . . . args ) {
displayMessage ( String . format ( s , args ) ) ;
}
@Override
public void formatStatus ( String s , Object . . . args ) {
displayStatus ( String . format ( s , args ) ) ;
}
@Override
public int getHeight ( ) {
return map . getHeight ( ) ;
}
@Override
public List < IItem > getLocalItems ( ) {
return map . getItems ( currentLocation ) ;
}
@Override
public ILocation getLocation ( ) {
return currentLocation ;
}
@Override
public ILocation getLocation ( GridDirection dir ) {
if ( currentLocation . canGo ( dir ) )
return currentLocation . go ( dir ) ;
else
return null ;
}
/ * *
* Return the game map . { @link IGameMap } gives us a few more details than
* { @link IMapView } ( write access to item lists ) ; the game needs this but
* individual items don ' t .
* /
@Override
public IMapView getMap ( ) {
return map ;
}
@Override
public List < GridDirection > getPossibleMoves ( ) {
2018-03-01 21:13:25 +01:00
List < GridDirection > moves = new ArrayList < > ( ) ;
for ( GridDirection dir : GridDirection . FOUR_DIRECTIONS ) {
if ( canGo ( dir ) ) {
moves . add ( dir ) ;
}
}
return moves ;
2018-02-28 23:51:40 +01:00
}
@Override
public List < ILocation > getVisible ( ) {
2018-03-09 21:29:56 +01:00
List < ILocation > neighbours = map . getNeighbourhood ( currentLocation , currentActor . getVision ( ) ) ;
List < ILocation > valid = new ArrayList < > ( ) ;
for ( ILocation neighbour : neighbours ) {
boolean blocked = false ;
for ( ILocation tile : currentLocation . gridLineTo ( neighbour ) ) {
if ( map . hasWall ( tile ) ) {
blocked = true ;
break ;
}
}
if ( ! blocked ) {
valid . add ( neighbour ) ;
}
}
return valid ;
2018-02-28 23:51:40 +01:00
}
@Override
public int getWidth ( ) {
return map . getWidth ( ) ;
}
2018-03-13 21:35:38 +01:00
public boolean keyPressed ( KeyCode code ) {
2018-02-28 23:51:40 +01:00
// only an IPlayer/human can handle keypresses, and only if it's the human's
// turn
if ( currentActor instanceof IPlayer ) {
2018-03-13 21:35:38 +01:00
return ! ( ( IPlayer ) currentActor ) . keyPressed ( this , code ) ;
2018-02-28 23:51:40 +01:00
}
2018-03-13 21:35:38 +01:00
return true ;
2018-02-28 23:51:40 +01:00
}
@Override
public ILocation move ( GridDirection dir ) {
if ( movePoints < 1 )
throw new IllegalMoveException ( " You're out of moves! " ) ;
ILocation newLoc = map . go ( currentLocation , dir ) ;
map . remove ( currentLocation , currentActor ) ;
map . add ( newLoc , currentActor ) ;
currentLocation = newLoc ;
movePoints - - ;
return currentLocation ;
}
@Override
public IItem pickUp ( IItem item ) {
if ( item ! = null & & map . has ( currentLocation , item ) ) {
2018-03-10 15:17:50 +01:00
if ( item instanceof IActor ) {
if ( item . getCurrentHealth ( ) / item . getMaxHealth ( ) < 3 ) {
map . remove ( currentLocation , item ) ;
return item ;
} else {
return null ;
}
} else if ( currentActor . getAttack ( ) > item . getDefence ( ) ) {
map . remove ( currentLocation , item ) ;
return item ;
} else {
return null ;
}
2018-02-28 23:51:40 +01:00
} else {
return null ;
}
}
@Override
public ITurtle getPainter ( ) {
return painter ;
}
@Override
public Printer getPrinter ( ) {
return printer ;
}
@Override
public int [ ] getFreeTextAreaBounds ( ) {
int [ ] area = new int [ 4 ] ;
area [ 0 ] = getWidth ( ) + 1 ;
area [ 1 ] = 1 ;
area [ 2 ] = printer . getLineWidth ( ) ;
area [ 3 ] = printer . getPageHeight ( ) - 5 ;
return area ;
}
@Override
public void clearFreeTextArea ( ) {
printer . clearRegion ( getWidth ( ) + 1 , 1 , printer . getLineWidth ( ) - getWidth ( ) , printer . getPageHeight ( ) - 5 ) ;
}
@Override
public void clearFreeGraphicsArea ( ) {
painter . as ( GraphicsContext . class ) . clearRect ( getWidth ( ) * printer . getCharWidth ( ) , 0 ,
painter . getWidth ( ) - getWidth ( ) * printer . getCharWidth ( ) ,
( printer . getPageHeight ( ) - 5 ) * printer . getCharHeight ( ) ) ;
}
@Override
public double [ ] getFreeGraphicsAreaBounds ( ) {
double [ ] area = new double [ 4 ] ;
area [ 0 ] = getWidth ( ) * printer . getCharWidth ( ) ;
area [ 1 ] = 0 ;
area [ 2 ] = painter . getWidth ( ) ;
area [ 3 ] = getHeight ( ) * printer . getCharHeight ( ) ;
return area ;
}
@Override
public IActor getActor ( ) {
return currentActor ;
}
public ILocation setCurrent ( IActor actor ) {
currentLocation = map . getLocation ( actor ) ;
if ( currentLocation ! = null ) {
currentActor = actor ;
movePoints = 1 ;
}
return currentLocation ;
}
public IActor setCurrent ( ILocation loc ) {
List < IActor > list = map . getActors ( loc ) ;
if ( ! list . isEmpty ( ) ) {
currentActor = list . get ( 0 ) ;
currentLocation = loc ;
movePoints = 1 ;
}
return currentActor ;
}
public IActor setCurrent ( int x , int y ) {
return setCurrent ( map . getLocation ( x , y ) ) ;
}
@Override
public Random getRandom ( ) {
return random ;
}
2018-03-08 23:38:59 +01:00
@Override
public GridDirection locationDirection ( ILocation start , ILocation target ) {
int targetX = target . getX ( ) , targetY = target . getY ( ) ;
int startX = start . getX ( ) , startY = start . getY ( ) ;
GridDirection dir = GridDirection . CENTER ;
if ( targetX > startX & & targetY > startY ) {
if ( Math . abs ( targetX - startX ) < Math . abs ( targetY - startY ) ) {
dir = GridDirection . SOUTH ;
} else {
dir = GridDirection . EAST ;
}
} else if ( targetX > startX & & targetY < startY ) {
if ( Math . abs ( targetX - startX ) < Math . abs ( targetY - startY ) ) {
dir = GridDirection . NORTH ;
} else {
dir = GridDirection . EAST ;
}
} else if ( targetX < startX & & targetY > startY ) {
if ( Math . abs ( targetX - startX ) < Math . abs ( targetY - startY ) ) {
dir = GridDirection . SOUTH ;
} else {
dir = GridDirection . WEST ;
}
} else if ( targetX < startX & & targetY < startY ) {
if ( Math . abs ( targetX - startX ) < Math . abs ( targetY - startY ) ) {
dir = GridDirection . NORTH ;
} else {
dir = GridDirection . WEST ;
}
} else if ( targetX > startX ) {
dir = GridDirection . EAST ;
} else if ( targetX < startX ) {
dir = GridDirection . WEST ;
} else if ( targetY > startY ) {
dir = GridDirection . SOUTH ;
} else if ( targetY < startY ) {
dir = GridDirection . NORTH ;
}
return dir ;
}
2018-02-28 23:51:40 +01:00
}