This is a project for a design and programming patterns subject in university. The idea is to try to program a card game based on the video game "The witcher 3".
Most of the code was written in a way were it wouldn't have to be written more than one time the best example is the player: When the player has to draw a card or play a card most of the work is done in the Deck and Hand classes.
Cards where created with future proofing. There is an abstract class with the common things that all cards need in a way that when gameplay has to be implemented the only thing thats going be changed is the methods inside the cards/deck/hand and that there won't be need of creating new classes.
Also most of the code is separated in many packages and scala files/classes/traits so it's easier to work with some specific function and it helps with order.
For the second assignment i created a UML diagram that can be found in the UML_Diagram.md file.
Related to the second assignment i tried to do most of the code in double dispatch so the code is more adaptable to future changes. The board is one that is divided in four zones and each one has a zone to each specific card. When a special ability is used the cards have their original value saved for when the effect is disipated.
Mostly that and that special Effects for climate cards are their classes because they already existed.
So now the project is divided in two packages "model" that is where most of the work made in the first and second assignment was done(and some more in this assignment so the design works because it had a mayor error related to cards not adding correctly to zones), and controller that is the work made in this third and final assignment, so basicly a MVC without the View.
The controller works in the following way, it first creates all the things needed to "play" the game. Then it waits for player input, and then the game follows depending on the players actions, computer ai, etc. Mostly i tried to make it really clear what every state does, computerTurn is the computers turn and contains most of what the computer does except play a random card, that is in PlayRandom, Start starts a Round, etc. They extend from a AbstracClass so I don't have to write change state in every class. The observer is there mostly so it sees the changes in states and notifies the player and itself
The code was designed in a way that if some functionality has to be added to a class it would be quick and not create problems.
In this project i used the following design patterns: Observer(To see the changes made with controller), Visitor(In Card,AbstractUnit,AbstractClimate,Melee,etc. So the tests and the intended method of effects and climate cards worked succesfully), Model-View-Controller(Its in the package names) and State(There is a package named State).
At the moment the tests are concerned with the classes working correctly. Most of the gameplay has been not implemented yet so interactions relating to the "game" haven't been tested completely. There are tests so the player can draw cards and that it's hand/deck changes correctly.
Also i put the names of characters from my favourite videogame(s) in the tests. Nothing related to how the code works but i figured it's interesting to tell. And i love that game.
There are two classes that are untestable :( ConsoleSubject and Play, because both expect a player input I ask if that could be considered when reviewing the project.
Most of the repository, documentation and the README.md is written in english to be more professional. The only parts not written in english are the ClimateCard names because i don't know their name in english.
classDiagram
%% ==================== CARD PACKAGE ====================
class Card {
<<trait>>
+getType() String
+getValue() Int
+playByComputer(player: Computer, board: Board) Unit
+tryPlayWeather(player: Computer, board: Board) Boolean
+playByPlayer(player: Human, board: Board) Unit
}
%% Unit Cards
class UnitCard {
<<trait>>
+getName() String
+getType() String
+getValue() Int
+getRange() String
+getSpecialAbility() Effect
+increaseValue(amount: Int) Unit
+decreaseValue(amount: Int) Unit
+setValue(amount: Int) Unit
}
class AbstractUnit {
<<abstract>>
-name: String
-cardType: String
-value: Int
-range: String
-specialAbility: Effect
-originalValue: Int
+getName() String
+getType() String
+getValue() Int
+getRange() String
+getSpecialAbility() Effect
+increaseValue(amount: Int) Unit
+decreaseValue(amount: Int) Unit
+setValue(amount: Int) Unit
+resetValue() Unit
+tryPlayWeather(computer: Computer, board: Board) Boolean
+playByComputer(player: Computer, board: Board) Unit
+playByPlayer(player: Human, board: Board) Unit
+equals(other: Any) Boolean
}
class Melee {
+name: String
+value: Int
+specialAbility: Effect
+isMelee() Boolean
+playByComputer(player: Computer, board: Board) Unit
+playByPlayer(player: Human, board: Board) Unit
}
class Ranged {
+name: String
+value: Int
+specialAbility: Effect
+isRanged() Boolean
+playByComputer(player: Computer, board: Board) Unit
+playByPlayer(player: Human, board: Board) Unit
}
class Siege {
+name: String
+value: Int
+specialAbility: Effect
+isSiege() Boolean
+playByComputer(player: Computer, board: Board) Unit
+playByPlayer(player: Human, board: Board) Unit
}
%% Climate Cards
class ClimateCard {
<<trait>>
+getClimate() String
+applyClimateEffect(board: Board) Unit
+unapplyClimateEffect(board: Board) Unit
}
class AbstractClimate {
<<abstract>>
-cardType: String
-climate: String
+getClimate() String
+getType() String
+getValue() Int
+applyClimateEffect(board: Board) Unit
+unapplyClimateEffect(board: Board) Unit
+playByComputer(player: Computer, board: Board) Unit
+playByPlayer(player: Human, board: Board) Unit
+tryPlayWeather(computer: Computer, board: Board) Boolean
+equals(other: Any) Boolean
}
class ClimaDespejado {
}
class EscarchaMordiente {
+applyClimateEffect(board: Board) Unit
+unapplyClimateEffect(board: Board) Unit
}
class LluviaTorrencial {
+applyClimateEffect(board: Board) Unit
+unapplyClimateEffect(board: Board) Unit
}
class NieblaImpenetrable {
+applyClimateEffect(board: Board) Unit
+unapplyClimateEffect(board: Board) Unit
}
%% Effects
class Effect {
<<trait>>
+applyPlayerEffect(board: Board, name: String) Boolean
+applyComputerEffect(board: Board, name: String) Boolean
+getEffect() String
}
class AbstractEffect {
<<abstract>>
-effect: String
+applyPlayerEffect(board: Board, name: String) Boolean
+applyComputerEffect(board: Board, name: String) Boolean
+getEffect() String
+equals(other: Any) Boolean
}
class NoEffect {
}
class VinculoEstrecho {
+applyPlayerEffect(board: Board, name: String) Boolean
+applyComputerEffect(board: Board, name: String) Boolean
}
class RefuerzoMoral {
+applyPlayerEffect(board: Board, name: String) Boolean
+applyComputerEffect(board: Board, name: String) Boolean
}
%% ==================== CHARACTER PACKAGE ====================
class Player {
<<trait>>
+getName() String
+getSection() String
+getGems() Int
+getDeck() Deck
+getHand() Hand
+isNpc() Boolean
+drawCard() Unit
+setGems(newGems: Int) Unit
+playMeleeCard(melee: Melee, board: Board) Unit
+playSiegeCard(siege: Siege, board: Board) Unit
+playRangedCard(ranged: Ranged, board: Board) Unit
+playWeatherCard(climateCard: ClimateCard, board: Board) Unit
}
class AbstractPlayer {
<<abstract>>
-name: String
-section: String
-gems: Int
-deck: Deck
-hand: Hand
+getName() String
+getSection() String
+getGems() Int
+getDeck() Deck
+getHand() Hand
+isNpc() Boolean
+drawCard() Unit
+takeDamage(dmg: Int) Unit
+heal(heal: Int) Unit
+setGems(newGems: Int) Unit
+equals(other: Any) Boolean
}
class Human {
+name: String
+gems: Int
+deck: Deck
+hand: Hand
+isNpc() Boolean
+playMeleeCard(melee: Melee, board: Board) Unit
+playSiegeCard(siege: Siege, board: Board) Unit
+playRangedCard(ranged: Ranged, board: Board) Unit
+playWeatherCard(climateCard: ClimateCard, board: Board) Unit
}
class Computer {
+name: String
+gems: Int
+deck: Deck
+hand: Hand
+isNpc() Boolean
+playMeleeCard(melee: Melee, board: Board) Unit
+playSiegeCard(siege: Siege, board: Board) Unit
+playRangedCard(ranged: Ranged, board: Board) Unit
+playWeatherCard(climateCard: ClimateCard, board: Board) Unit
}
%% ==================== BUILD PACKAGE ====================
class Build {
<<trait>>
+getCards() List~Card~
+addCard(card: Card) Unit
+removeCard(card: Card) Unit
}
class AbstractBuild {
<<abstract>>
-playerBuild: List~Card~
+getCards() List~Card~
+addCard(card: Card) Unit
+removeCard(card: Card) Unit
+equals(other: Any) Boolean
}
class Deck {
+Deck: List~Card~
+setCards(cards: List~Card~) Unit
+shuffle() Unit
}
class Hand {
+Hand: List~Card~
}
class Surface~T~ {
-board: List~T~
+getCards() List~T~
+addCard(card: Card) Unit
+removeCard(card: Card) Unit
+equals(other: Any) Boolean
}
%% ==================== BOARD PACKAGE ====================
class Board {
-siegeZone: SiegeZone
-meleeZone: MeleeZone
-rangedZone: RangedZone
-climateZone: ClimateZone
+getMeleeZone() MeleeZone
+getRangedZone() RangedZone
+getSiegeZone() SiegeZone
+getWeatherZone() ClimateZone
}
class Zone~T~ {
<<trait>>
+getZone() String
+getPlayerCards() Surface~T~
+getComputerCards() Surface~T~
+playCardComputer(card: T) Unit
+playCardPlayer(card: T) Unit
}
class AbstractZone~T~ {
<<abstract>>
-zone: String
-playerCardList: Surface~T~
-computerCardList: Surface~T~
+getZone() String
+getPlayerCards() Surface~T~
+getComputerCards() Surface~T~
+playCardComputer(card: T) Unit
+playCardPlayer(card: T) Unit
+equals(other: Any) Boolean
}
class MeleeZone {
-playerMeleeList: Surface~Melee~
-computerMeleeList: Surface~Melee~
}
class RangedZone {
-playerRangedList: Surface~Ranged~
-computerRangedList: Surface~Ranged~
}
class SiegeZone {
-playerSiegeList: Surface~Siege~
-computerSiegeList: Surface~Siege~
}
class ClimateZone {
-card: Surface~ClimateCard~
+getPlayerCards() Surface~ClimateCard~
+getComputerCards() Surface~ClimateCard~
+playCardComputer(climate: ClimateCard) Unit
+playCardPlayer(climate: ClimateCard) Unit
}
%% ==================== CONTROLLER PACKAGE ====================
class GameController {
-state: GameState
-observer: ConsoleObserver
-humanPlayer: Option~Human~
-computerPlayer: Option~Computer~
-board: Board
-currentRound: Int
-humanPassed: Boolean
-computerPassed: Boolean
+setState(newState: GameState) Unit
+play() Unit
+start() Unit
+end() Unit
+draw() Unit
+update() Unit
+win() Unit
+beginGame(human: Human, computer: Computer) Unit
+startNewRound() Unit
+endRound() Unit
+calculatePlayerStrength() Int
+calculateComputerStrength() Int
+playerPass() Unit
+computerPass() Unit
+playerHasCards() Boolean
+computerHasCards() Boolean
+equals(obj: Any) Boolean
}
class GameState {
<<trait>>
+changeState(newState: GameState) Unit
+getState() GameState
+getController() GameController
+play(context: GameController) Unit
+start(context: GameController) Unit
+end(context: GameController) Unit
+update() Unit
}
class State {
<<abstract>>
+controller: GameController
+changeState(newState: GameState) Unit
+getState() GameState
+getController() GameController
+play(context: GameController) Unit
+start(context: GameController) Unit
+end(context: GameController) Unit
+update() Unit
+equals(other: Any) Boolean
}
class Start {
+play(context: GameController) Unit
+start(context: GameController) Unit
+update() Unit
}
class PlayerTurn {
+play(context: GameController) Unit
+end(context: GameController) Unit
}
class ComputerTurn {
+play(context: GameController) Unit
+end(context: GameController) Unit
}
class PlayRandom {
+play(context: GameController) Unit
+end(context: GameController) Unit
}
class Play {
+play(context: GameController) Unit
+end(context: GameController) Unit
}
class Win {
+play(context: GameController) Unit
+start(context: GameController) Unit
}
class InvalidTransitionException {
<<exception>>
+action: String
}
%% ==================== OBSERVER PACKAGE ====================
class Observer~T~ {
<<trait>>
+update(subject: Subject~T~, response: T) Unit
}
class ConsoleObserver {
+update(subject: Subject~String~, response: String) Unit
}
class Subject~T~ {
<<trait>>
+attach(observer: Observer~T~) Unit
+detach(observer: Observer~T~) Unit
+notifyObservers(notification: T) Unit
}
class BaseSubject~T~ {
-observers: Set~Observer~T~~
+attach(observer: Observer~T~) Unit
+detach(observer: Observer~T~) Unit
+notifyObservers(notification: T) Unit
}
class ConsoleSubject {
+run() Unit
}
class ExitSignal {
<<exception>>
}
%% ==================== RELATIONSHIPS ====================
%% Card Hierarchy
Card <|.. UnitCard
Card <|.. ClimateCard
UnitCard <|.. AbstractUnit
AbstractUnit <|-- Melee
AbstractUnit <|-- Ranged
AbstractUnit <|-- Siege
ClimateCard <|.. AbstractClimate
AbstractClimate <|-- ClimaDespejado
AbstractClimate <|-- EscarchaMordiente
AbstractClimate <|-- LluviaTorrencial
AbstractClimate <|-- NieblaImpenetrable
%% Effect Hierarchy
Effect <|.. AbstractEffect
AbstractEffect <|-- NoEffect
AbstractEffect <|-- VinculoEstrecho
AbstractEffect <|-- RefuerzoMoral
%% Player Hierarchy
Player <|.. AbstractPlayer
AbstractPlayer <|-- Human
AbstractPlayer <|-- Computer
%% Build Hierarchy
Build <|.. AbstractBuild
AbstractBuild <|-- Deck
AbstractBuild <|-- Hand
Build <|.. Surface~T~
%% Zone Hierarchy
Zone~T~ <|.. AbstractZone~T~
AbstractZone~T~ <|-- MeleeZone
AbstractZone~T~ <|-- RangedZone
AbstractZone~T~ <|-- SiegeZone
AbstractZone~T~ <|-- ClimateZone
%% State Pattern
GameState <|.. State
State <|-- Start
State <|-- PlayerTurn
State <|-- ComputerTurn
State <|-- PlayRandom
State <|-- Play
State <|-- Win
%% Observer Pattern
Observer~T~ <|.. ConsoleObserver
Subject~T~ <|.. BaseSubject~T~
BaseSubject~T~ <|-- GameController
BaseSubject~String~ <|-- ConsoleSubject
%% Composition Relationships
AbstractUnit *-- Effect
AbstractPlayer *-- Deck
AbstractPlayer *-- Hand
Board *-- MeleeZone
Board *-- RangedZone
Board *-- SiegeZone
Board *-- ClimateZone
AbstractZone~T~ *-- Surface~T~
GameController *-- Board
GameController *-- GameState
GameController o-- Human
GameController o-- Computer
ConsoleSubject o-- Observer~String~
%% Dependencies
Player ..> Board
ClimateCard ..> Board
Effect ..> Board
Card ..> Human
Card ..> Computer
Card ..> Board
State ..> GameController