Compare commits
22 Commits
webapplica
...
archive/ma
| Author | SHA1 | Date | |
|---|---|---|---|
| bef96ba7e3 | |||
| c0dadf8927 | |||
| 1f377de0f4 | |||
| 6c31fa0538 | |||
| 729a4eec6b | |||
| 72fcf783b8 | |||
| 1517d0c006 | |||
| 7f765b4514 | |||
| 03f1811ab4 | |||
| 63689b7a26 | |||
| 92e4851219 | |||
| c168ae7dc0 | |||
| ccf44ede41 | |||
| d71809d6f4 | |||
| 82245d6bcc | |||
| d8576f669a | |||
|
|
cfe27f1e78 | ||
|
|
b33ab184d2 | ||
|
|
b17c5160e9 | ||
| 7458464dd6 | |||
| f8c337fad1 | |||
| 3357fb7310 |
46
.gitea/ISSUE_TEMPLATE/epic.md
Normal file
46
.gitea/ISSUE_TEMPLATE/epic.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: "Epic"
|
||||
about: "A large initiative or feature that consists of multiple User Stories or Subtasks"
|
||||
title: "[Epic] <Epic title>"
|
||||
labels: ["Type/Epic"]
|
||||
---
|
||||
|
||||
## 🧩 Epic Summary
|
||||
Provide a clear summary of what this Epic aims to achieve.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals
|
||||
- [ ] Goal 1
|
||||
- [ ] Goal 2
|
||||
- [ ] Goal 3
|
||||
|
||||
---
|
||||
|
||||
## 📋 Description
|
||||
Describe the high-level context, purpose, and expected outcome of this Epic.
|
||||
|
||||
---
|
||||
|
||||
## 🧵 Related User Stories / Subtasks
|
||||
Link to related issues here:
|
||||
- [ ] #<user-story-1>
|
||||
- [ ] #<user-story-2>
|
||||
- [ ] #<subtask-1>
|
||||
|
||||
---
|
||||
|
||||
## 📅 Milestone / Timeline
|
||||
If applicable, note any key dates or milestones:
|
||||
- Target Start:
|
||||
- Target Completion:
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Risks / Dependencies
|
||||
List any major risks or dependencies that could affect delivery.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
Define the success metrics or completion definition for the Epic.
|
||||
35
.gitea/ISSUE_TEMPLATE/subtask.md
Normal file
35
.gitea/ISSUE_TEMPLATE/subtask.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "Subtask"
|
||||
about: "A smaller task that contributes to a User Story or Epic"
|
||||
title: "[Subtask] <Task title>"
|
||||
labels: ["Type/Subtask"]
|
||||
---
|
||||
|
||||
## 🧾 Description
|
||||
Briefly describe what needs to be done for this subtask.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Linked Issues
|
||||
- Parent Story: #<user-story-number>
|
||||
- Related Epic: #<epic-number>
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Steps / Tasks
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
---
|
||||
|
||||
## ✅ Definition of Done
|
||||
What must be true for this subtask to be considered complete:
|
||||
- [ ] Code implemented
|
||||
- [ ] Tests passed
|
||||
- [ ] Reviewed and merged
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Estimated Size
|
||||
Use label: `Size/XS` | `Size/S` | `Size/M` | `Size/L` | `Size/XL`
|
||||
42
.gitea/ISSUE_TEMPLATE/user_story.md
Normal file
42
.gitea/ISSUE_TEMPLATE/user_story.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "User Story"
|
||||
about: "A feature or requirement from the user's perspective"
|
||||
title: "[Story] <User story title>"
|
||||
labels: ["Type/User Story"]
|
||||
---
|
||||
|
||||
## 🧍♂️ User Story
|
||||
**As a** [type of user]
|
||||
**I want to** [perform an action]
|
||||
**So that** [achieve a goal or value]
|
||||
|
||||
---
|
||||
|
||||
## 📋 Description
|
||||
Provide additional context or business logic for this story.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Acceptance Criteria
|
||||
List the specific, measurable criteria that define when this story is done:
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
- [ ] Criterion 3
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Implementation Notes
|
||||
Include technical notes, design references, or constraints.
|
||||
|
||||
---
|
||||
|
||||
## 🧵 Linked Issues
|
||||
- Parent Epic: #<epic-number>
|
||||
- Related Subtasks:
|
||||
- [ ] #<subtask-1>
|
||||
- [ ] #<subtask-2>
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Estimated Size
|
||||
Use label: `Size/XS` | `Size/S` | `Size/M` | `Size/L` | `Size/XL`
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,4 +1,4 @@
|
||||
[submodule "knockoutwhist"]
|
||||
path = knockoutwhist
|
||||
branch = webapplication
|
||||
branch = main
|
||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
||||
|
||||
@@ -19,7 +19,6 @@ lazy val commonSettings = Seq(
|
||||
.map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName)
|
||||
},
|
||||
libraryDependencies += guice,
|
||||
Test / testOptions += Tests.Filter(_.equals("de.knockoutwhist.TestSequence")),
|
||||
coverageEnabled := true,
|
||||
coverageFailOnMinimum := true,
|
||||
coverageMinimumStmtTotal := 85,
|
||||
@@ -29,7 +28,8 @@ lazy val commonSettings = Seq(
|
||||
lazy val knockoutwhist = project.in(file("knockoutwhist"))
|
||||
.settings(
|
||||
commonSettings,
|
||||
mainClass := Some("de.knockoutwhist.KnockOutWhist")
|
||||
mainClass := Some("de.knockoutwhist.KnockOutWhist"),
|
||||
coverageExcludedPackages := "de.knockoutwhist.ui.*;de.knockoutwhist.utils.gui.*"
|
||||
)
|
||||
|
||||
lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
|
||||
116
conventionalcommit.json
Normal file
116
conventionalcommit.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"$schema": "https://github.com/lppedd/idea-conventional-commit/raw/master/src/main/resources/defaults/conventionalcommit.schema.json",
|
||||
"types": {
|
||||
"refactor": {
|
||||
"description": "Changes which neither fix a bug nor add a feature"
|
||||
},
|
||||
"fix": {
|
||||
"description": "Changes which patch a bug"
|
||||
},
|
||||
"feat": {
|
||||
"description": "Changes which introduce a new feature"
|
||||
},
|
||||
"build": {
|
||||
"description": "Changes which affect the build system or external dependencies.<br>Example scopes: gulp, broccoli, npm",
|
||||
"scopes": {
|
||||
"npm": {},
|
||||
"gulp": {},
|
||||
"broccoli": {}
|
||||
}
|
||||
},
|
||||
"chore": {
|
||||
"description": "Changes which are not user-facing"
|
||||
},
|
||||
"style": {
|
||||
"description": "Changes which do not affect code logic, such as whitespaces, formatting, missing semicolons"
|
||||
},
|
||||
"test": {
|
||||
"description": "Changes which add missing tests or fix existing ones"
|
||||
},
|
||||
"docs": {
|
||||
"description": "Changes which affect documentation"
|
||||
},
|
||||
"perf": {
|
||||
"description": "Changes which improve performance"
|
||||
},
|
||||
"ci": {
|
||||
"description": "Changes which affect CI configuration files and scripts.<br>Example scopes: travis, circle, browser-stack, sauce-labs"
|
||||
},
|
||||
"revert": {
|
||||
"description": "Changes which revert a previous commit"
|
||||
}
|
||||
},
|
||||
"commonScopes": {
|
||||
"api": {
|
||||
"description": "Changes related to the API"
|
||||
},
|
||||
"auth": {
|
||||
"description": "Changes related to authentication"
|
||||
},
|
||||
"config": {
|
||||
"description": "Changes related to configuration"
|
||||
},
|
||||
"db": {
|
||||
"description": "Changes related to the database"
|
||||
},
|
||||
"docs": {
|
||||
"description": "Changes related to documentation"
|
||||
},
|
||||
"ui": {
|
||||
"description": "Changes related to the user interface"
|
||||
},
|
||||
"ux": {
|
||||
"description": "Changes related to the user experience"
|
||||
},
|
||||
"build": {
|
||||
"description": "Changes related to the build system"
|
||||
},
|
||||
"ci": {
|
||||
"description": "Changes related to continuous integration"
|
||||
},
|
||||
"deps": {
|
||||
"description": "Changes related to dependencies"
|
||||
},
|
||||
"promotion": {
|
||||
"description": "Kargo promotion changes"
|
||||
},
|
||||
"base": {
|
||||
"description": "Changes related to the core functionality"
|
||||
},
|
||||
"release": {
|
||||
"description": "A release commit"
|
||||
}
|
||||
},
|
||||
"footerTypes": {
|
||||
"BREAKING-CHANGE": {
|
||||
"description": "The commit introduces breaking changes"
|
||||
},
|
||||
"Closes": {
|
||||
"description": "The commit closes issues or pull requests"
|
||||
},
|
||||
"Implements": {
|
||||
"description": "The commit implements new features"
|
||||
},
|
||||
"Author": {
|
||||
"description": "The commit's author"
|
||||
},
|
||||
"Co-authored-by": {
|
||||
"description": "The specified person co-authored the commit's changes"
|
||||
},
|
||||
"Signed-off-by": {
|
||||
"description": "Certifies the committer has the rights to submit the work under the project's license or agrees to some contributor representation, such as a Developer Certificate of Origin"
|
||||
},
|
||||
"Reviewed-by": {
|
||||
"description": "The specified person reviewed and is completely satisfied with the commit's changes"
|
||||
},
|
||||
"Tested-by": {
|
||||
"description": "The specified person applied the commit's changes and found them to have the desired effect"
|
||||
},
|
||||
"Acked-by": {
|
||||
"description": "The specified person liked the commit's changes"
|
||||
},
|
||||
"Refs": {
|
||||
"description": "The commit references another commit by its hash ID.<br>For multiple hash IDs, use a comma as separator"
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule knockoutwhist updated: 48cd4d3956...fbc0ea2277
6
knockoutwhistweb/app/assets/stylesheets/dark-mode.less
Normal file
6
knockoutwhistweb/app/assets/stylesheets/dark-mode.less
Normal file
@@ -0,0 +1,6 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-image: url('/assets/images/background.png');
|
||||
--color: white;
|
||||
}
|
||||
}
|
||||
4
knockoutwhistweb/app/assets/stylesheets/light-mode.less
Normal file
4
knockoutwhistweb/app/assets/stylesheets/light-mode.less
Normal file
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--background-image: url('/assets/images/img.png');
|
||||
--color: black;
|
||||
}
|
||||
133
knockoutwhistweb/app/assets/stylesheets/main.less
Normal file
133
knockoutwhistweb/app/assets/stylesheets/main.less
Normal file
@@ -0,0 +1,133 @@
|
||||
@import "light-mode.less";
|
||||
@import "dark-mode.less";
|
||||
|
||||
@background-image: var(--background-image);
|
||||
@color: var(--color);
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
body {
|
||||
background-image: @background-image;
|
||||
background-size: 100vw 100vh;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
#sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
h1 {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
}
|
||||
#textanimation {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
#sessions a, h1, p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
#ingame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
#playercards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 20%;
|
||||
img {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
&:nth-child(1) { animation-delay: 0.5s; }
|
||||
&:nth-child(2) { animation-delay: 1s; }
|
||||
&:nth-child(3) { animation-delay: 1.5s; }
|
||||
&:nth-child(4) { animation-delay: 2s; }
|
||||
&:nth-child(5) { animation-delay: 2.5s; }
|
||||
&:nth-child(6) { animation-delay: 3s; }
|
||||
&:nth-child(7) { animation-delay: 3.5s; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#cardsplayed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 10%;
|
||||
min-height: 10%
|
||||
}
|
||||
#playedcardplayer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
#playedcardplayer p {
|
||||
font-size: 12px;
|
||||
height: 4%;
|
||||
}
|
||||
#playedcardplayer img {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#firstCard {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#firstCardObject {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4%;
|
||||
}
|
||||
#firstCardObject img{
|
||||
height: 90%;
|
||||
}
|
||||
#firstCardObject p{
|
||||
height: 10%;
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
#trumpsuit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 4%;
|
||||
}
|
||||
#nextPlayers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 0;
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
#invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
#selecttrumpsuit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
#rules {
|
||||
color: @color;
|
||||
font-size: 1.5em;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package components
|
||||
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
import controllers.WebUI
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.utils.events.EventListener
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.SimpleSession
|
||||
import com.google.inject.{Guice, Injector}
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.KnockOutWhist
|
||||
import de.knockoutwhist.components.Configuration
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import di.KnockOutWebConfigurationModule
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
import play.api.*
|
||||
import play.twirl.api.Html
|
||||
|
||||
import java.util.UUID
|
||||
@@ -39,25 +41,53 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e
|
||||
Redirect("/sessions")
|
||||
}
|
||||
}
|
||||
|
||||
def rules(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.rules.apply())
|
||||
}
|
||||
}
|
||||
def sessions(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.sessions.apply(PodGameManager.listSessions().map(f => f.toString)))
|
||||
Ok(views.html.sessions.apply(PodGameManager.listSessions()))
|
||||
}
|
||||
}
|
||||
|
||||
def ingame(id: String): Action[AnyContent] = {
|
||||
val uuid: UUID = UUID.fromString(id)
|
||||
if (PodGameManager.identify(uuid).isEmpty) {
|
||||
Action { implicit request =>
|
||||
return Action { implicit request =>
|
||||
NotFound(views.html.tui.apply(List(Html(s"<p>Session with id $id not found!</p>"))))
|
||||
}
|
||||
} else {
|
||||
val session = PodGameManager.identify(uuid).get
|
||||
Action { implicit request =>
|
||||
Ok(views.html.tui.apply(session.asInstanceOf[SimpleSession].get()))
|
||||
val player = session.asInstanceOf[AdvancedSession].player
|
||||
val logic = WebUI.logic.get.asInstanceOf[BaseGameLogic]
|
||||
if (logic.getCurrentState == Lobby) {
|
||||
|
||||
} else if (logic.getCurrentState == InGame) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.ingame.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == SelectTrump) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.selecttrump.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == TieBreak) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.tie.apply(player, logic))
|
||||
}
|
||||
}
|
||||
}
|
||||
Action { implicit request =>
|
||||
InternalServerError("Oops")
|
||||
}
|
||||
//if (logic.getCurrentState == Lobby) {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
//} else {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,8 +30,8 @@ object PodGameManager {
|
||||
sessions.foreach(session => session._2.updatePlayer(event))
|
||||
}
|
||||
|
||||
def listSessions(): List[UUID] = {
|
||||
sessions.keys.toList
|
||||
def listSessions(): List[PlayerSession] = {
|
||||
sessions.values.toList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.SimpleSession
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import de.knockoutwhist.events.*
|
||||
import de.knockoutwhist.events.ERROR_STATUS.*
|
||||
import de.knockoutwhist.events.GLOBAL_STATUS.*
|
||||
import de.knockoutwhist.events.PLAYER_STATUS.*
|
||||
import de.knockoutwhist.events.ROUND_STATUS.{PLAYERS_OUT, SHOW_START_ROUND, WON_ROUND}
|
||||
import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent}
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.events.ui.GameState.{INGAME, MAIN_MENU}
|
||||
import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent}
|
||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.rounds.Match
|
||||
import de.knockoutwhist.ui.UI
|
||||
@@ -22,54 +18,30 @@ object WebUI extends CustomThread with EventListener with UI {
|
||||
setName("WebUI")
|
||||
|
||||
var init = false
|
||||
private var internState: GameState = GameState.NO_SET
|
||||
var logic: Option[GameLogic] = None
|
||||
|
||||
var latestOutput: String = ""
|
||||
|
||||
override def instance: CustomThread = WebUI
|
||||
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
runLater {
|
||||
event match {
|
||||
case event: RenderHandEvent =>
|
||||
PodGameManager.transmit(event.player.id, event)
|
||||
case event: ShowTieCardsEvent =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowGlobalStatus =>
|
||||
if (event.status == TECHNICAL_MATCH_STARTED) {
|
||||
val matchImpl = event.objects.head.asInstanceOf[Match]
|
||||
for (player <- matchImpl.totalplayers) {
|
||||
PodGameManager.addSession(SimpleSession(player.id, List()))
|
||||
}
|
||||
} else {
|
||||
PodGameManager.transmitAll(event)
|
||||
}
|
||||
case event: ShowPlayerStatus =>
|
||||
PodGameManager.transmit(event.player.id, event)
|
||||
case event: ShowRoundStatus =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowErrorStatus =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowCurrentTrickEvent =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: GameStateUpdateEvent =>
|
||||
if (internState != event.gameState) {
|
||||
internState = event.gameState
|
||||
if (event.gameState == MAIN_MENU) {
|
||||
PodGameManager.clearSessions()
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
event match {
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == Lobby && event.newState == InGame) {
|
||||
val match1: Option[Match] = logic.get.asInstanceOf[BaseGameLogic].getCurrentMatch
|
||||
val players: List[AbstractPlayer] = match1.get.totalplayers
|
||||
players.map(player => PodGameManager.addSession(AdvancedSession(player.id, player)))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
override def initial: Boolean = {
|
||||
override def initial(gameLogic: GameLogic): Boolean = {
|
||||
if (init) {
|
||||
return false
|
||||
}
|
||||
init = true
|
||||
this.logic = Some(gameLogic)
|
||||
start()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package controllers.sessions
|
||||
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class AdvancedSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||
|
||||
def name: String = player.name
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import java.util.UUID
|
||||
trait PlayerSession {
|
||||
|
||||
def id: UUID
|
||||
def name: String
|
||||
def updatePlayer(event: SimpleEvent): Unit
|
||||
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
package controllers.sessions
|
||||
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand}
|
||||
import de.knockoutwhist.events.ERROR_STATUS.*
|
||||
import de.knockoutwhist.events.GLOBAL_STATUS.*
|
||||
import de.knockoutwhist.events.PLAYER_STATUS.*
|
||||
import de.knockoutwhist.events.ROUND_STATUS.*
|
||||
import de.knockoutwhist.events.{ShowErrorStatus, ShowGlobalStatus, ShowPlayerStatus, ShowRoundStatus}
|
||||
import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent}
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
import util.WebUIUtils
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class SimpleSession(id: UUID, private var output: List[Html]) extends PlayerSession {
|
||||
def get(): List[Html] = {
|
||||
output
|
||||
}
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
case event: RenderHandEvent =>
|
||||
renderHand(event)
|
||||
case event: ShowTieCardsEvent =>
|
||||
showtiecardseventmethod(event)
|
||||
case event: ShowGlobalStatus =>
|
||||
showglobalstatusmethod(event)
|
||||
case event: ShowPlayerStatus =>
|
||||
showplayerstatusmethod(event)
|
||||
case event: ShowRoundStatus =>
|
||||
showroundstatusmethod(event)
|
||||
case event: ShowErrorStatus =>
|
||||
showerrstatmet(event)
|
||||
case event: ShowCurrentTrickEvent =>
|
||||
showcurtrevmet(event)
|
||||
}
|
||||
}
|
||||
|
||||
private def clear(): Unit = {
|
||||
output = List()
|
||||
}
|
||||
|
||||
private def renderHand(event: RenderHandEvent): Unit = {
|
||||
output = output :++ WebUICards.renderHandEvent(event.hand)
|
||||
output = output :+ Html("<br>")
|
||||
}
|
||||
|
||||
private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = {
|
||||
var l = List[Html]()
|
||||
for ((player, card) <- event.card) {
|
||||
l = l :+ Html(s"<p>${player.name}:</p>")
|
||||
l = l :+ WebUIUtils.cardtoImage(card)
|
||||
l = l :+ Html("<br>")
|
||||
}
|
||||
output = output :++ l
|
||||
output = output :+ Html("<br>")
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case SHOW_TIE =>
|
||||
println("It's a tie! Let's cut to determine the winner.")
|
||||
Some(true)
|
||||
case SHOW_TIE_WINNER =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} wins the cut!")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_TIE_TIE =>
|
||||
println("It's a tie again! Let's cut again.")
|
||||
Some(true)
|
||||
case SHOW_START_MATCH =>
|
||||
clear()
|
||||
println("Starting a new match...")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
case SHOW_TYPE_PLAYERS =>
|
||||
println("Please enter the names of the players, separated by a comma.")
|
||||
Some(true)
|
||||
case SHOW_FINISHED_MATCH =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
clear()
|
||||
println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = {
|
||||
val player = event.player
|
||||
event.status match {
|
||||
case SHOW_PLAY_CARD =>
|
||||
println("Which card do you want to play?")
|
||||
Some(true)
|
||||
case SHOW_DOG_PLAY_CARD =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Boolean]) {
|
||||
None
|
||||
} else {
|
||||
println("You are using your dog life. Do you want to play your final card now?")
|
||||
if (event.objects.head.asInstanceOf[Boolean]) {
|
||||
println("You have to play your final card this round!")
|
||||
println("Please enter y to play your final card.")
|
||||
Some(true)
|
||||
} else {
|
||||
println("Please enter y/n to play your final card.")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
case SHOW_TIE_NUMBERS =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Int]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${player.name} enter a number between 1 and ${event.objects.head.asInstanceOf[Int]}.")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_TRUMPSUIT_OPTIONS =>
|
||||
println("Which suit do you want to pick as the next trump suit?")
|
||||
println("1: Hearts")
|
||||
println("2: Diamonds")
|
||||
println("3: Clubs")
|
||||
println("4: Spades")
|
||||
println()
|
||||
Some(true)
|
||||
case SHOW_NOT_PLAYED =>
|
||||
println(s"Player ${event.player} decided to not play his card")
|
||||
Some(true)
|
||||
case SHOW_WON_PLAYER_TRICK =>
|
||||
println(s"${event.player.name} won the trick.")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case SHOW_TURN =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"It's ${event.objects.head.asInstanceOf[AbstractPlayer].name} turn.")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_START_ROUND =>
|
||||
clear()
|
||||
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
case WON_ROUND =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} won the round.")
|
||||
Some(true)
|
||||
}
|
||||
case PLAYERS_OUT =>
|
||||
println("The following players are out of the game:")
|
||||
event.currentRound.playersout.foreach(p => {
|
||||
println(p.name)
|
||||
})
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case INVALID_NUMBER =>
|
||||
println("Please enter a valid number.")
|
||||
Some(true)
|
||||
case NOT_A_NUMBER =>
|
||||
println("Please enter a number.")
|
||||
Some(true)
|
||||
case INVALID_INPUT =>
|
||||
println("Please enter a valid input")
|
||||
Some(true)
|
||||
case INVALID_NUMBER_OF_PLAYERS =>
|
||||
println("Please enter at least two names.")
|
||||
Some(true)
|
||||
case IDENTICAL_NAMES =>
|
||||
println("Please enter unique names.")
|
||||
Some(true)
|
||||
case INVALID_NAME_FORMAT =>
|
||||
println("Please enter valid names. Those can not be empty, shorter than 2 or longer then 10 characters.")
|
||||
Some(true)
|
||||
case WRONG_CARD =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Card]) {
|
||||
None
|
||||
} else {
|
||||
println(f"You have to play a card of suit: ${event.objects.head.asInstanceOf[Card].suit}\n")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
|
||||
clear()
|
||||
val sb = new StringBuilder()
|
||||
sb.append("Current Trick:\n")
|
||||
sb.append("Trump-Suit: " + event.round.trumpSuit + "\n")
|
||||
if (event.trick.firstCard.isDefined) {
|
||||
sb.append(s"Suit to play: ${event.trick.firstCard.get.suit}\n")
|
||||
}
|
||||
for ((card, player) <- event.trick.cards) {
|
||||
sb.append(s"${player.name} played ${card.toString}\n")
|
||||
}
|
||||
println(sb.toString())
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def println(s: String): Unit = {
|
||||
var html = List[Html]()
|
||||
for (line <- s.split("\n")) {
|
||||
html = html :+ Html(line)
|
||||
html = html :+ Html("<br>")
|
||||
}
|
||||
output = output :++ html
|
||||
}
|
||||
|
||||
private def println(): Unit = {
|
||||
output = output :+ Html("<br>")
|
||||
}
|
||||
|
||||
object WebUICards {
|
||||
def renderCardAsString(card: Card): Vector[String] = {
|
||||
val lines = "│ │"
|
||||
if (card.cardValue == CardValue.Ten) {
|
||||
return Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${card.cardValue.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.suit.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.cardValue.cardType()}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${card.cardValue.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.suit.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.cardValue.cardType()}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
|
||||
def renderHandEvent(hand: Hand): List[Html] = {
|
||||
hand.cards.map(WebUIUtils.cardtoImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import de.knockoutwhist.cards.CardValue.{Ace, Eight, Five, Four, Jack, King, Nine, Queen, Seven, Six, Ten, Three, Two}
|
||||
import de.knockoutwhist.cards.CardValue.*
|
||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
|
||||
50
knockoutwhistweb/app/views/ingame.scala.html
Normal file
50
knockoutwhistweb/app/views/ingame.scala.html
Normal file
@@ -0,0 +1,50 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Ingame") {
|
||||
<div id="ingame">
|
||||
<h1>Knockout Whist</h1>
|
||||
<div id="nextPlayers">
|
||||
<p>Next Player:</p>
|
||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
||||
</div>
|
||||
<div id="firstCard">
|
||||
<div id="trumpsuit">
|
||||
<p>Trumpsuit: </p>
|
||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
||||
</div>
|
||||
<div id="firstCardObject">
|
||||
<p>First Card</p>
|
||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
||||
} else {
|
||||
@views.html.output.card.apply("images/cards/1B.png")("Blank Card")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>@logic.getCurrentPlayer.get has to play a card!</p>
|
||||
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
|
||||
<p>Cards played</p>
|
||||
} else {
|
||||
<p id="invisible">Cards played</p>
|
||||
}
|
||||
|
||||
<div id="cardsplayed">
|
||||
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(cardplayed)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p>Your cards</p>
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -13,8 +13,8 @@
|
||||
<title>@title</title>
|
||||
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
||||
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@* And here's where we render the `Html` object containing
|
||||
* the page content. *@
|
||||
|
||||
63
knockoutwhistweb/app/views/rules.scala.html
Normal file
63
knockoutwhistweb/app/views/rules.scala.html
Normal file
@@ -0,0 +1,63 @@
|
||||
@()
|
||||
|
||||
@main("Rules") {
|
||||
<div id="rules">
|
||||
<table>
|
||||
<caption>Rules Overview and Equipment</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Section</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Players</td>
|
||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aim</td>
|
||||
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Equipment</td>
|
||||
<td>A standard 52-card pack is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Card Ranks</td>
|
||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (First Hand)</td>
|
||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (Subsequent Hands)</td>
|
||||
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Play</td>
|
||||
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning a Trick</td>
|
||||
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Leading Trumps</td>
|
||||
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Knockout</td>
|
||||
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning the Game</td>
|
||||
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<td>Dog Life</td>
|
||||
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
27
knockoutwhistweb/app/views/selecttrump.scala.html
Normal file
27
knockoutwhistweb/app/views/selecttrump.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit">
|
||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
||||
<p>Available trumpsuits are displayed below:</p>
|
||||
<div id="playercards">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
||||
</div>
|
||||
<p>Your cards</p>
|
||||
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
@(toRender: List[String])
|
||||
@(sessions: List[controllers.sessions.PlayerSession])
|
||||
|
||||
@main("Sessions") {
|
||||
<div id="sessions">
|
||||
@for(line <- toRender) {
|
||||
<a href="@routes.HomeController.ingame(line)">@line</a><br>
|
||||
<h1>Knockout Whist sessions</h1>
|
||||
<p id="textanimation">Please select your session to jump inside the game!</p>
|
||||
@for(session <- sessions) {
|
||||
<a id="textanimation" href="@routes.HomeController.ingame(session.id.toString)">@session.name</a><br>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
27
knockoutwhistweb/app/views/tie.scala.html
Normal file
27
knockoutwhistweb/app/views/tie.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="tie">
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>The last Round was tied between
|
||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
||||
@players
|
||||
}
|
||||
</p>
|
||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
||||
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p>
|
||||
} else {
|
||||
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p>
|
||||
<p>Currently picked Cards:</p>
|
||||
<div id="cardsplayed">
|
||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -10,3 +10,5 @@ GET /sessions controllers.HomeController.sessions()
|
||||
GET /ingame/:id controllers.HomeController.ingame(id: String)
|
||||
# Map static resources from the /public folder to the /assets URL path
|
||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||
|
||||
GET /rules controllers.HomeController.rules()
|
||||
|
||||
BIN
knockoutwhistweb/public/images/background.png
Normal file
BIN
knockoutwhistweb/public/images/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
BIN
knockoutwhistweb/public/images/img.png
Normal file
BIN
knockoutwhistweb/public/images/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
@@ -1,9 +1,9 @@
|
||||
package controllers
|
||||
|
||||
import org.scalatestplus.play._
|
||||
import org.scalatestplus.play.guice._
|
||||
import play.api.test._
|
||||
import play.api.test.Helpers._
|
||||
import org.scalatestplus.play.*
|
||||
import org.scalatestplus.play.guice.*
|
||||
import play.api.test.*
|
||||
import play.api.test.Helpers.*
|
||||
|
||||
/**
|
||||
* Add your spec here.
|
||||
|
||||
@@ -3,4 +3,6 @@ addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.18.0")
|
||||
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.1")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1")
|
||||
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1")
|
||||
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2")
|
||||
|
||||
Reference in New Issue
Block a user