Improve engine performances
This commit is contained in:
parent
2aca12c94d
commit
7ede900287
|
@ -2,24 +2,40 @@ package lila.benchmark
|
||||||
|
|
||||||
import annotation.tailrec
|
import annotation.tailrec
|
||||||
import com.google.caliper.Param
|
import com.google.caliper.Param
|
||||||
|
import ornicar.scalalib.OrnicarValidation
|
||||||
|
|
||||||
import lila.chess.Game
|
import lila.chess.{ Game, Pos }
|
||||||
import lila.chess.Pos._
|
import lila.chess.Pos._
|
||||||
|
|
||||||
// a caliper benchmark is a class that extends com.google.caliper.Benchmark
|
// a caliper benchmark is a class that extends com.google.caliper.Benchmark
|
||||||
// the SimpleScalaBenchmark trait does it and also adds some convenience functionality
|
// the SimpleScalaBenchmark trait does it and also adds some convenience functionality
|
||||||
class Benchmark extends SimpleScalaBenchmark {
|
class Benchmark extends SimpleScalaBenchmark with OrnicarValidation {
|
||||||
|
|
||||||
@Param(Array("100"))
|
@Param(Array("100"))
|
||||||
val length: Int = 0
|
val length: Int = 0
|
||||||
|
|
||||||
def timeImmortal(reps: Int) = repeat(reps) {
|
def timeImmortal(reps: Int) = repeat(reps) {
|
||||||
val s = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6)
|
val s = playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6)
|
||||||
if (s.isFailure) throw new Exception("success")
|
if (s.isFailure) throw new Exception("success")
|
||||||
"haha"
|
"haha"
|
||||||
}
|
}
|
||||||
|
|
||||||
def timeDeepBlue(reps: Int) = repeat(reps) {
|
//def timeDeepBlue(reps: Int) = repeat(reps) {
|
||||||
Game().playMoves(E2 -> E4, C7 -> C5, C2 -> C3, D7 -> D5, E4 -> D5, D8 -> D5, D2 -> D4, G8 -> F6, G1 -> F3, C8 -> G4, F1 -> E2, E7 -> E6, H2 -> H3, G4 -> H5, E1 -> G1, B8 -> C6, C1 -> E3, C5 -> D4, C3 -> D4, F8 -> B4)
|
//playMoves(E2 -> E4, C7 -> C5, C2 -> C3, D7 -> D5, E4 -> D5, D8 -> D5, D2 -> D4, G8 -> F6, G1 -> F3, C8 -> G4, F1 -> E2, E7 -> E6, H2 -> H3, G4 -> H5, E1 -> G1, B8 -> C6, C1 -> E3, C5 -> D4, C3 -> D4, F8 -> B4)
|
||||||
|
//}
|
||||||
|
|
||||||
|
def playMoves(moves: (Pos, Pos)*): Valid[Game] = {
|
||||||
|
val game = moves.foldLeft(success(Game()): Valid[Game]) {
|
||||||
|
(vg, move) ⇒ {
|
||||||
|
vg flatMap { g ⇒
|
||||||
|
// will be called to pass as playable squares to the client
|
||||||
|
g.situation.destinations
|
||||||
|
g.playMove(move._1, move._2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (game.isFailure) throw new RuntimeException("Failure!")
|
||||||
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,5 +168,5 @@ case class Actor(piece: Piece, pos: Pos, board: Board) {
|
||||||
private def history = board.history
|
private def history = board.history
|
||||||
private def friends = board occupation color
|
private def friends = board occupation color
|
||||||
private def enemies = board occupation !color
|
private def enemies = board occupation !color
|
||||||
private val pawnDir: Direction = if (color == White) _.up else _.down
|
private lazy val pawnDir: Direction = if (color == White) _.up else _.down
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,6 @@ case class Board(pieces: Map[Pos, Piece], history: History) {
|
||||||
if (pieces contains at) None
|
if (pieces contains at) None
|
||||||
else Some(copy(pieces = pieces + ((at, piece))))
|
else Some(copy(pieces = pieces + ((at, piece))))
|
||||||
|
|
||||||
def takeValid(at: Pos): Valid[Board] = take(at) toSuccess ("No piece at " + at + " to take")
|
|
||||||
|
|
||||||
def take(at: Pos): Option[Board] = pieces get at map { piece ⇒
|
def take(at: Pos): Option[Board] = pieces get at map { piece ⇒
|
||||||
copy(pieces = pieces - at)
|
copy(pieces = pieces - at)
|
||||||
}
|
}
|
||||||
|
@ -87,8 +85,6 @@ case class Board(pieces: Map[Pos, Piece], history: History) {
|
||||||
|
|
||||||
def updateHistory(f: History ⇒ History) = copy(history = f(history))
|
def updateHistory(f: History ⇒ History) = copy(history = f(history))
|
||||||
|
|
||||||
def as(c: Color) = Situation(this, c)
|
|
||||||
|
|
||||||
def count(p: Piece) = pieces.values count (_ == p)
|
def count(p: Piece) = pieces.values count (_ == p)
|
||||||
|
|
||||||
def visual = Visual >> this
|
def visual = Visual >> this
|
||||||
|
|
|
@ -7,26 +7,15 @@ case class Game(
|
||||||
player: Color,
|
player: Color,
|
||||||
reversedPgnMoves: List[String] = Nil) {
|
reversedPgnMoves: List[String] = Nil) {
|
||||||
|
|
||||||
def this() = this(Board.empty, White)
|
|
||||||
|
|
||||||
def playMoves(moves: (Pos, Pos)*): Valid[Game] =
|
|
||||||
moves.foldLeft(success(this): Valid[Game]) { (sit, move) ⇒
|
|
||||||
sit flatMap { s ⇒ s.playMove(move._1, move._2) }
|
|
||||||
}
|
|
||||||
|
|
||||||
def playMove(from: Pos, to: Pos, promotion: PromotableRole = Queen): Valid[Game] = for {
|
def playMove(from: Pos, to: Pos, promotion: PromotableRole = Queen): Valid[Game] = for {
|
||||||
move ← situation.move(from, to, promotion)
|
move ← situation.move(from, to, promotion)
|
||||||
} yield copy(
|
} yield {
|
||||||
board = move.after,
|
val newGame = copy(board = move.after, player = !player)
|
||||||
player = !player,
|
val pgnMove = PgnDump.move(situation, move, newGame.situation)
|
||||||
reversedPgnMoves = (PgnDump move move) :: reversedPgnMoves
|
newGame.copy(reversedPgnMoves = pgnMove :: reversedPgnMoves)
|
||||||
)
|
}
|
||||||
|
|
||||||
val players = List(White, Black)
|
lazy val situation = Situation(board, player)
|
||||||
|
|
||||||
def situation = board as player
|
|
||||||
|
|
||||||
def as(c: Color) = copy(player = c)
|
|
||||||
|
|
||||||
def pgnMoves = reversedPgnMoves.reverse
|
def pgnMoves = reversedPgnMoves.reverse
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,6 @@ case class Move(
|
||||||
|
|
||||||
def withHistory(h: History) = copy(after = after withHistory h)
|
def withHistory(h: History) = copy(after = after withHistory h)
|
||||||
|
|
||||||
def situation = before as piece.color
|
|
||||||
|
|
||||||
// does this move check the opponent?
|
|
||||||
def checks: Boolean = (after as !color).check
|
|
||||||
|
|
||||||
// does this move checkmate the opponent?
|
|
||||||
def checkMates: Boolean = (after as !color).checkMate
|
|
||||||
|
|
||||||
// does this move capture an opponent piece?
|
// does this move capture an opponent piece?
|
||||||
def captures = capture.isDefined
|
def captures = capture.isDefined
|
||||||
|
|
||||||
|
|
|
@ -41,12 +41,9 @@ sealed case class Pos private (x: Int, y: Int) {
|
||||||
|
|
||||||
object Pos {
|
object Pos {
|
||||||
|
|
||||||
private val bounds: Set[Int] = (1 to 8) toSet
|
def makePos(x: Int, y: Int): Option[Pos] = allCoords get (x, y)
|
||||||
|
|
||||||
def makePos(x: Int, y: Int): Option[Pos] =
|
def unsafe(x: Int, y: Int): Pos = allCoords((x, y))
|
||||||
if (bounds(x) && bounds(y)) Some(Pos(x, y)) else None
|
|
||||||
|
|
||||||
def unsafe(x: Int, y: Int): Pos = Pos(x, y)
|
|
||||||
|
|
||||||
def xToString(x: Int) = (96 + x).toChar.toString
|
def xToString(x: Int) = (96 + x).toChar.toString
|
||||||
|
|
||||||
|
@ -117,5 +114,14 @@ object Pos {
|
||||||
|
|
||||||
val allKeys: Map[String, Pos] = Map("a1" -> A1, "a2" -> A2, "a3" -> A3, "a4" -> A4, "a5" -> A5, "a6" -> A6, "a7" -> A7, "a8" -> A8, "b1" -> B1, "b2" -> B2, "b3" -> B3, "b4" -> B4, "b5" -> B5, "b6" -> B6, "b7" -> B7, "b8" -> B8, "c1" -> C1, "c2" -> C2, "c3" -> C3, "c4" -> C4, "c5" -> C5, "c6" -> C6, "c7" -> C7, "c8" -> C8, "d1" -> D1, "d2" -> D2, "d3" -> D3, "d4" -> D4, "d5" -> D5, "d6" -> D6, "d7" -> D7, "d8" -> D8, "e1" -> E1, "e2" -> E2, "e3" -> E3, "e4" -> E4, "e5" -> E5, "e6" -> E6, "e7" -> E7, "e8" -> E8, "f1" -> F1, "f2" -> F2, "f3" -> F3, "f4" -> F4, "f5" -> F5, "f6" -> F6, "f7" -> F7, "f8" -> F8, "g1" -> G1, "g2" -> G2, "g3" -> G3, "g4" -> G4, "g5" -> G5, "g6" -> G6, "g7" -> G7, "g8" -> G8, "h1" -> H1, "h2" -> H2, "h3" -> H3, "h4" -> H4, "h5" -> H5, "h6" -> H6, "h7" -> H7, "h8" -> H8)
|
val allKeys: Map[String, Pos] = Map("a1" -> A1, "a2" -> A2, "a3" -> A3, "a4" -> A4, "a5" -> A5, "a6" -> A6, "a7" -> A7, "a8" -> A8, "b1" -> B1, "b2" -> B2, "b3" -> B3, "b4" -> B4, "b5" -> B5, "b6" -> B6, "b7" -> B7, "b8" -> B8, "c1" -> C1, "c2" -> C2, "c3" -> C3, "c4" -> C4, "c5" -> C5, "c6" -> C6, "c7" -> C7, "c8" -> C8, "d1" -> D1, "d2" -> D2, "d3" -> D3, "d4" -> D4, "d5" -> D5, "d6" -> D6, "d7" -> D7, "d8" -> D8, "e1" -> E1, "e2" -> E2, "e3" -> E3, "e4" -> E4, "e5" -> E5, "e6" -> E6, "e7" -> E7, "e8" -> E8, "f1" -> F1, "f2" -> F2, "f3" -> F3, "f4" -> F4, "f5" -> F5, "f6" -> F6, "f7" -> F7, "f8" -> F8, "g1" -> G1, "g2" -> G2, "g3" -> G3, "g4" -> G4, "g5" -> G5, "g6" -> G6, "g7" -> G7, "g8" -> G8, "h1" -> H1, "h2" -> H2, "h3" -> H3, "h4" -> H4, "h5" -> H5, "h6" -> H6, "h7" -> H7, "h8" -> H8)
|
||||||
|
|
||||||
|
val allCoords: Map[(Int, Int), Pos] = {
|
||||||
|
for {
|
||||||
|
x ← 1 to 8
|
||||||
|
xString = xToString(x)
|
||||||
|
y ← 1 to 8
|
||||||
|
key = xString + y.toString
|
||||||
|
} yield (x, y) -> allKeys(key)
|
||||||
|
} toMap
|
||||||
|
|
||||||
def all = allKeys.values
|
def all = allKeys.values
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ case class Situation(board: Board, color: Color) {
|
||||||
case actor if actor.moves.nonEmpty ⇒ actor.pos -> actor.moves
|
case actor if actor.moves.nonEmpty ⇒ actor.pos -> actor.moves
|
||||||
} toMap
|
} toMap
|
||||||
|
|
||||||
|
lazy val destinations: Map[Pos, List[Pos]] = moves mapValues { ms => ms map (_.dest) }
|
||||||
|
|
||||||
lazy val check: Boolean = board kingPosOf color map { king ⇒
|
lazy val check: Boolean = board kingPosOf color map { king ⇒
|
||||||
board actorsOf !color exists (_ threatens king)
|
board actorsOf !color exists (_ threatens king)
|
||||||
} getOrElse false
|
} getOrElse false
|
||||||
|
@ -33,9 +35,7 @@ case class Situation(board: Board, color: Color) {
|
||||||
b3 ← b2.place(color - promotion, to)
|
b3 ← b2.place(color - promotion, to)
|
||||||
} yield m.copy(after = b3, promotion = Some(promotion))
|
} yield m.copy(after = b3, promotion = Some(promotion))
|
||||||
|
|
||||||
} toSuccess "Invalid move %s->%s".format(from, to).wrapNel
|
} toSuccess "Invalid move %s %s".format(from, to).wrapNel
|
||||||
|
|
||||||
def as(c: Color) = copy(color = c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Situation {
|
object Situation {
|
||||||
|
|
|
@ -3,27 +3,21 @@ package format
|
||||||
|
|
||||||
object PgnDump {
|
object PgnDump {
|
||||||
|
|
||||||
def move(m: Move): String = {
|
def move(situation: Situation, data: Move, nextSituation: Situation): String = {
|
||||||
import m._
|
import data._
|
||||||
|
|
||||||
def disambiguate = {
|
|
||||||
val candidates = situation.actors filter { a ⇒
|
|
||||||
a.pos != orig && a.piece.role == piece.role && (a.destinations contains dest)
|
|
||||||
}
|
|
||||||
if (candidates.isEmpty) ""
|
|
||||||
else if (candidates exists (_.pos ?| orig)) orig.file + orig.rank else orig.file
|
|
||||||
}
|
|
||||||
def capturing = if (captures) "x" else ""
|
|
||||||
def checking = if (checkMates) "#" else if (checks) "+" else ""
|
|
||||||
|
|
||||||
((promotion, piece.role) match {
|
((promotion, piece.role) match {
|
||||||
case _ if castle ⇒ if (orig ?> dest) "O-O-O" else "O-O"
|
case _ if castle ⇒ if (orig ?> dest) "O-O-O" else "O-O"
|
||||||
case _ if enpassant ⇒ orig.file + 'x' + dest.rank
|
case _ if enpassant ⇒ orig.file + 'x' + dest.rank
|
||||||
case (Some(promotion), _) ⇒ dest.key + promotion.pgn
|
case (Some(promotion), _) ⇒ dest.key + promotion.pgn
|
||||||
case (_, Pawn) if captures ⇒ orig.file + 'x' + dest.key
|
case (_, Pawn) ⇒ (if (captures) orig.file + "x" else "") + dest.key
|
||||||
case (_, Pawn) ⇒ dest.key
|
case (_, role) ⇒ role.pgn + {
|
||||||
case (_, role) ⇒ role.pgn + disambiguate + capturing + dest.key
|
val candidates = situation.actors filter { a ⇒
|
||||||
case _ ⇒ "?"
|
a.piece.role == piece.role && a.pos != orig && (a.destinations contains dest)
|
||||||
}) + checking
|
}
|
||||||
|
if (candidates.isEmpty) ""
|
||||||
|
else if (candidates exists (_.pos ?| orig)) orig.file + orig.rank else orig.file
|
||||||
|
} + (if (captures) "x" else "") + dest.key
|
||||||
|
case _ ⇒ "?"
|
||||||
|
}) + (if (nextSituation.check) if (nextSituation.checkMate) "#" else "+" else "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class BoardTest extends LilaTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
"allow a piece to be taken" in {
|
"allow a piece to be taken" in {
|
||||||
board takeValid A1 must beSuccess.like {
|
board take A1 must beSome.like {
|
||||||
case b ⇒ b(A1) must beNone
|
case b ⇒ b(A1) must beNone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,21 @@ trait LilaTest
|
||||||
|
|
||||||
implicit def stringToBoard(str: String): Board = Visual << str
|
implicit def stringToBoard(str: String): Board = Visual << str
|
||||||
|
|
||||||
|
implicit def stringToSituationBuilder(str: String) = new {
|
||||||
|
|
||||||
|
def as(color: Color): Situation = Situation(Visual << str, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
implicit def richGame(game: Game) = new {
|
||||||
|
|
||||||
|
def as(color: Color): Game = game.copy(player = color)
|
||||||
|
|
||||||
|
def playMoves(moves: (Pos, Pos)*): Valid[Game] =
|
||||||
|
moves.foldLeft(success(game): Valid[Game]) { (vg, move) ⇒
|
||||||
|
vg flatMap { g ⇒ g.playMove(move._1, move._2) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def bePoss(poss: Pos*): Matcher[Option[Iterable[Pos]]] = beSome.like {
|
def bePoss(poss: Pos*): Matcher[Option[Iterable[Pos]]] = beSome.like {
|
||||||
case p ⇒ sortPoss(p.toList) must_== sortPoss(poss.toList)
|
case p ⇒ sortPoss(p.toList) must_== sortPoss(poss.toList)
|
||||||
}
|
}
|
||||||
|
|
12
chess/src/test/scala/PlayOneMoveTest.scala
Normal file
12
chess/src/test/scala/PlayOneMoveTest.scala
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package lila.chess
|
||||||
|
|
||||||
|
import Pos._
|
||||||
|
|
||||||
|
class PlayOneMoveTest extends LilaTest {
|
||||||
|
|
||||||
|
"playing a move" should {
|
||||||
|
"only process things once" in {
|
||||||
|
Game().playMoves(E2 -> E4) must beSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,17 +5,19 @@ import Pos._
|
||||||
|
|
||||||
class PgnDumpTest extends LilaTest {
|
class PgnDumpTest extends LilaTest {
|
||||||
|
|
||||||
"complete game dump" should {
|
val gioachineGreco = Game().playMoves(D2 -> D4, D7 -> D5, C2 -> C4, D5 -> C4, E2 -> E3, B7 -> B5, A2 -> A4, C7 -> C6, A4 -> B5, C6 -> B5, D1 -> F3)
|
||||||
"only moves" in {
|
|
||||||
|
val peruvianImmortal = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6)
|
||||||
|
|
||||||
|
"dump a game to pgn" should {
|
||||||
|
"move list" in {
|
||||||
"Gioachine Greco" in {
|
"Gioachine Greco" in {
|
||||||
val game = Game().playMoves(D2 -> D4, D7 -> D5, C2 -> C4, D5 -> C4, E2 -> E3, B7 -> B5, A2 -> A4, C7 -> C6, A4 -> B5, C6 -> B5, D1 -> F3)
|
gioachineGreco map (_.pgnMoves) must beSuccess.like {
|
||||||
game map (_.pgnMoves) must beSuccess.like {
|
|
||||||
case ms ⇒ ms must_== ("d4 d5 c4 dxc4 e3 b5 a4 c6 axb5 cxb5 Qf3" split ' ').toSeq
|
case ms ⇒ ms must_== ("d4 d5 c4 dxc4 e3 b5 a4 c6 axb5 cxb5 Qf3" split ' ').toSeq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Peruvian Immortal" in {
|
"Peruvian Immortal" in {
|
||||||
val game = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6)
|
peruvianImmortal map (_.pgnMoves) must beSuccess.like {
|
||||||
game map (_.pgnMoves) must beSuccess.like {
|
|
||||||
case ms ⇒ ms must_== ("e4 d5 exd5 Qxd5 Nc3 Qa5 d4 c6 Nf3 Bg4 Bf4 e6 h3 Bxf3 Qxf3 Bb4 Be2 Nd7 a3 O-O-O axb4 Qxa1+ Kd2 Qxh1 Qxc6+ bxc6 Ba6#" split ' ').toSeq
|
case ms ⇒ ms must_== ("e4 d5 exd5 Qxd5 Nc3 Qa5 d4 c6 Nf3 Bg4 Bf4 e6 h3 Bxf3 Qxf3 Bb4 Be2 Nd7 a3 O-O-O axb4 Qxa1+ Kd2 Qxh1 Qxc6+ bxc6 Ba6#" split ' ').toSeq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,8 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
|
||||||
|
|
||||||
lazy val benchmark = Project("benchmark", file("benchmark"), settings = Project.defaultSettings).settings(
|
lazy val benchmark = Project("benchmark", file("benchmark"), settings = Project.defaultSettings).settings(
|
||||||
fork in run := true,
|
fork in run := true,
|
||||||
libraryDependencies := Seq(instrumenter, gson),
|
libraryDependencies := Seq(scalalib, instrumenter, gson),
|
||||||
resolvers := Seq(codahale, sonatype),
|
resolvers := Seq(iliaz, codahale, sonatype),
|
||||||
shellPrompt := ShellPrompt.buildShellPrompt,
|
shellPrompt := ShellPrompt.buildShellPrompt,
|
||||||
scalacOptions := Seq("-deprecation", "-unchecked"),
|
scalacOptions := Seq("-deprecation", "-unchecked"),
|
||||||
// we need to add the runtime classpath as a "-cp" argument
|
// we need to add the runtime classpath as a "-cp" argument
|
||||||
|
|
Loading…
Reference in a new issue