diff --git a/app/src/main/java/net/pokeranalytics/android/util/MathUtils.kt b/app/src/main/java/net/pokeranalytics/android/util/MathUtils.kt index 0ad2022d..134a7382 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/MathUtils.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/MathUtils.kt @@ -56,7 +56,6 @@ class MathUtils { return null } - } private class EllipseQuarter(val x1: Float, val y1: Float, val xRadius: Float, val yRadius: Float, val direction: Direction): Path { @@ -96,7 +95,7 @@ class MathUtils { } - companion object{ + companion object { private fun ellipseCircumference(a: Float, b: Float): Float { return Math.PI.toFloat() * sqrt(2f * (a.pow(2) + b.pow(2))) diff --git a/gradle.properties b/gradle.properties index 84ada6d0..8ba4fc71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,4 +35,6 @@ android.enableBuildCache=true # Enable simple gradle caching org.gradle.caching=true +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false diff --git a/kmmshared/build.gradle.kts b/kmmshared/build.gradle.kts new file mode 100644 index 00000000..3abc34b4 --- /dev/null +++ b/kmmshared/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + android() + ios { + binaries { + framework { + baseName = "kmmshared" + } + } + } + sourceSets { + val commonMain by getting + val androidMain by getting + val iosMain by getting + } +} + +android { + compileSdkVersion(29) + defaultConfig { + minSdkVersion(24) + targetSdkVersion(29) + } + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") +} \ No newline at end of file diff --git a/kmmshared/src/androidMain/AndroidManifest.xml b/kmmshared/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..fa804145 --- /dev/null +++ b/kmmshared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/kmmshared/src/androidMain/kotlin/com/staxriver/kmmshared/Platform.kt b/kmmshared/src/androidMain/kotlin/com/staxriver/kmmshared/Platform.kt new file mode 100644 index 00000000..efb723d8 --- /dev/null +++ b/kmmshared/src/androidMain/kotlin/com/staxriver/kmmshared/Platform.kt @@ -0,0 +1,5 @@ +package com.staxriver.kmmshared + +actual class Platform actual constructor() { + actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" +} \ No newline at end of file diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Geometry.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Geometry.kt new file mode 100644 index 00000000..8912209d --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Geometry.kt @@ -0,0 +1,31 @@ +package com.staxriver.kmmshared + +import kotlin.math.pow +import kotlin.math.sqrt + +data class Point(var x: Float, var y: Float) { + + fun length(): Float { + return sqrt(x.pow(2) + y.pow(2)) + } + +} + +data class Size(var width: Float, var height: Float) +data class Circle(var x: Float, var y: Float, var radius: Float) +data class TextPoint(var x: Float, var y: Float, var fontSize: Float) + +data class Rect(var bottom: Float = 0.0f, var top: Float = 0.0f, var left: Float = 0.0f, var right: Float = 0.0f) { + + val width: Float + get() { return right - left } + + val height: Float + get() { return top - bottom } + + val centerX: Float + get() { return (this.left + this.right) / 2 } + + val centerY: Float + get() { return (this.top + this.bottom) / 2 } +} diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Greeting.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Greeting.kt new file mode 100644 index 00000000..aa47851c --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Greeting.kt @@ -0,0 +1,7 @@ +package com.staxriver.kmmshared + +class Greeting { + fun greeting(): String { + return "Hello, ${Platform().platform}!" + } +} \ No newline at end of file diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/HandHistoryInformer.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/HandHistoryInformer.kt new file mode 100644 index 00000000..e84f97f4 --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/HandHistoryInformer.kt @@ -0,0 +1,9 @@ +package com.staxriver.kmmshared + +interface HandHistoryInformer { + + val playerCount: Int + val maxCards: Int + fun cardsCountForPlayer(position: Int): Int? + +} \ No newline at end of file diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/MathUtils.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/MathUtils.kt new file mode 100644 index 00000000..81c607f4 --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/MathUtils.kt @@ -0,0 +1,141 @@ +package com.staxriver.kmmshared + +import kotlin.math.PI +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.math.tan + +class MathUtils { + + private interface Path { + val length: Float + fun pointForDistance(distance: Float): Point + } + + class Line(val x1: Float, val y1: Float, val x2: Float, val y2: Float): Path { + override val length: Float + get() { + return Point(x2 - x1, y2 - y1).length() + } + + override fun pointForDistance(distance: Float): Point { + val ratio = distance / this.length + return Point(x1 + ratio * (x2 - x1), y1 + ratio * (y2 - y1)) + } + + val slope: Float + get() { return (x2 - x1) / (y2 - y1) } + + private fun intersects(line: Line): Point? { + + val s1_x = this.x2 - this.x1 + val s1_y = this.y2 - this.y1 + val s2_x = line.x2 - line.x1 + val s2_y = line.y2 - line.y1 + + val s = (-s1_y * (this.x1 - line.x1) + s1_x * (this.y1 - line.y1)) / (-s2_x * s1_y + s1_x * s2_y); + val t = ( s2_x * (this.y1 - line.y1) - s2_y * (this.x1 - line.x1)) / (-s2_x * s1_y + s1_x * s2_y); + + return if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { + Point(this.x1 + (t * s1_x), this.y1 + (t * s1_y)) + } else { + null + } + } + + fun intersects(rect: Rect): Point? { + val p1 = this.intersects(Line(rect.left, rect.top, rect.right, rect.top)) + if (p1 != null) return p1 + val p2 = this.intersects(Line(rect.right, rect.top, rect.right, rect.bottom)) + if (p2 != null) return p2 + val p3 = this.intersects(Line(rect.left, rect.bottom, rect.right, rect.bottom)) + if (p3 != null) return p3 + val p4 = this.intersects(Line(rect.left, rect.top, rect.left, rect.bottom)) + if (p4 != null) return p4 + return null + } + + } + + private class EllipseQuarter(val x1: Float, val y1: Float, val xRadius: Float, val yRadius: Float, val direction: Direction): Path { + + enum class Direction { SOUTH_EAST, SOUTH_WEST, NORTH_EAST, NORTH_WEST; + val east: Boolean + get() { return this == SOUTH_EAST || this == NORTH_EAST } + val angle: Float + get() { + return when (this) { + NORTH_EAST -> PI.toFloat() * 0.5f + NORTH_WEST -> PI.toFloat() + SOUTH_EAST -> 0f + SOUTH_WEST -> PI.toFloat() * 1.5f + } + } + } + + override val length: Float + get() { + return ellipseCircumference(this.xRadius, this.yRadius) / 4f + } + + override fun pointForDistance(distance: Float): Point { + val angle = PI.toFloat() / 2 * distance / length + return pointForAngle(angle) + } + + private fun pointForAngle(angle: Float): Point { + val baseAngle = direction.angle - angle + val denominator = sqrt(yRadius.pow(2) + xRadius.pow(2) * tan(baseAngle).pow(2)) + val x = xRadius * yRadius / denominator + val y = xRadius * yRadius * tan(baseAngle) / denominator + return if (this.direction.east) Point(x1 + x, y1 - y) + else Point(x1 - x, y1 + y) + } + + } + + companion object { + + private fun ellipseCircumference(a: Float, b: Float): Float { + return PI.toFloat() * sqrt(2f * (a.pow(2) + b.pow(2))) + } + + fun positionsAroundRoundedRectangle(numberOfPlayers: Int, rect: Rect, xRadius: Float, yRadius: Float): List> { + + val segments = listOf( + Line(rect.centerX, rect.bottom, rect.left + xRadius, rect.bottom), + EllipseQuarter(rect.left + xRadius, rect.bottom - yRadius, xRadius, yRadius, EllipseQuarter.Direction.SOUTH_WEST), + Line(rect.left, rect.bottom - yRadius, rect.left, rect.top + yRadius), + EllipseQuarter(rect.left + xRadius, rect.top + yRadius, xRadius, yRadius, EllipseQuarter.Direction.NORTH_WEST), + Line(rect.left + xRadius, rect.top, rect.right - xRadius, rect.top), + EllipseQuarter(rect.right - xRadius, rect.top + yRadius, xRadius, yRadius, EllipseQuarter.Direction.NORTH_EAST), + Line(rect.right, rect.top + yRadius, rect.right, rect.bottom - yRadius), + EllipseQuarter(rect.right - xRadius, rect.bottom - yRadius, xRadius, yRadius, EllipseQuarter.Direction.SOUTH_EAST), + Line(rect.right - xRadius, rect.bottom, rect.centerX, rect.bottom) + ) + + val perimeter = segments.sumByDouble { it.length.toDouble() } + val distancePerPlayer = perimeter / numberOfPlayers + + val playerPoints = mutableListOf>() + for (i in 0 until numberOfPlayers) { + var distanceFromOrigin = i * distancePerPlayer.toFloat() + var pathIndex = 0 + + while (distanceFromOrigin > segments[pathIndex].length) { + distanceFromOrigin -= segments[pathIndex].length + pathIndex++ + } + + val p = segments[pathIndex].pointForDistance(distanceFromOrigin) + // 2 first and 2 last are at the bottom + val bottom = pathIndex < 2 || pathIndex > 6 + playerPoints.add(Pair(p, bottom)) + } + + return playerPoints + } + + } + +} \ No newline at end of file diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/PAException.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/PAException.kt new file mode 100644 index 00000000..1281272d --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/PAException.kt @@ -0,0 +1,3 @@ +package com.staxriver.kmmshared + +class PAException(message: String) : Exception(message) diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Platform.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Platform.kt new file mode 100644 index 00000000..b3bb76e2 --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/Platform.kt @@ -0,0 +1,5 @@ +package com.staxriver.kmmshared + +expect class Platform() { + val platform: String +} \ No newline at end of file diff --git a/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/TableDimension.kt b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/TableDimension.kt new file mode 100644 index 00000000..441f530f --- /dev/null +++ b/kmmshared/src/commonMain/kotlin/com/staxriver/kmmshared/TableDimension.kt @@ -0,0 +1,192 @@ +package com.staxriver.kmmshared + +class TableDimension(var width: Float, var height: Float, var handHistory: HandHistoryInformer) { + + var tableRect = Rect() + var tableCornerXRadius = 0f + var tableCornerYRadius = 0f + var cardSpecs: Size = Size(0f, 0f) + var cardRadius = 0f + + private var playerStackRects = mutableListOf() + private var playerCircles = mutableListOf() + private var playerNamePoints = mutableListOf() + private var playerStackPoints = mutableListOf() + private var playerActionPoints = mutableListOf() + private var playerCardRects = mutableListOf>() + + private var chipCircles = mutableListOf() + private var chipTextPoints = mutableListOf() + + var boardCardRects = mutableListOf() + + private var dealerBottomOriented = true + + val dealerCircle: Circle + get() { + val rect = this.playerStackRects.last() + val radius = (rect.bottom - rect.top) / 4 + val width = rect.right - rect.left + val x: Float; val y: Float + if (this.dealerBottomOriented) { + x = rect.left + y = (rect.top + rect.bottom) / 2f + } else { + x = rect.left + width / 6.0f + y = rect.bottom + } + return Circle(x, y, radius) + } + + private var playerItemsHeight = 10f + private var playerItemsWidth = 10f + private var chipRadius = 10f + private var paddingPercentage = 0.8f + private var cardsPaddingPercentage = 0.9f + + private var tableHPadding = 0f + private var tableVPadding = 0f + +// var showVillainHands: Boolean = true + + var potTextPoint = TextPoint(0f, 0f, 0f) + var totalPotTextPoint = TextPoint(0f, 0f, 0f) + var potChipCircle = Circle(0f, 0f, 0f) + + var tableStrokeWidth = 30f + var playerStrokeWidth = 8f + var cardStrokeWidth = 8f + + private val centerX: Float + get() { return this.width / 2f } + private val centerY: Float + get() { return this.height / 2f } + + val maxCards: Int = this.handHistory.maxCards + val players: Int = this.handHistory.playerCount + + private fun setDimension(width: Float, height: Float) { + +// Timber.d("Setting dimensions...") + this.width = width + this.height = height + + val maxPlayerCards = this.maxCards + val hwRatio = 1.3f + val portrait = height > width + + val base = 3.3f + this.players / 20.0f + val playerPerColumn = if (portrait) base * hwRatio else base + val playerPerRow = if (portrait) base else base * hwRatio + +// val grid = when (this.handHistory.numberOfPlayers) { +// 9, 10 -> Pair(4f, 5f) +// else -> Pair(3.7f, 4.7f) +// } + +// Timber.d("playerPerRow = $playerPerRow, playerPerColumn = $playerPerColumn") +// val playerPerColumn = if (portrait) grid.second else grid.first +// val playerPerRow = if (portrait) grid.first else grid.second + + val padding = if (portrait) 0.8f else 0.95f + this.tableHPadding = width / playerPerRow / 2 * padding + this.tableVPadding = height / playerPerColumn * 0.8f + + this.tableRect = Rect(tableHPadding, tableVPadding, width - tableHPadding, height - tableVPadding) + + this.tableCornerXRadius = this.tableRect.width / 3 + this.tableCornerYRadius = this.tableRect.height / 3 + + this.tableStrokeWidth = tableHPadding / 5f + this.playerStrokeWidth = this.tableStrokeWidth / 4f + this.cardStrokeWidth = this.tableStrokeWidth / 4f + + // pz for Player Zone + val pzHeight = height / playerPerColumn + + this.playerItemsHeight = pzHeight / 3 + this.playerItemsWidth = this.tableHPadding * 2 * this.paddingPercentage + this.chipRadius = this.playerItemsHeight / 4 + + val cardWPaddingWidth = this.playerItemsWidth * 1.4f / this.maxCards + val cardWidth = cardWPaddingWidth * this.cardsPaddingPercentage + this.cardSpecs = Size(cardWidth, cardWidth * 1.75f) + this.cardRadius = cardWidth / 4 + + // Board cards rectangles + val bcCenterY = centerY + cardSpecs.height / 2 + val bcTop = bcCenterY - cardSpecs.height / 2 + val bcBottom = bcCenterY + cardSpecs.height / 2 + val boardCards = 5 + for (i in 0 until boardCards) { + val bcLeft = centerX - (boardCards / 2f - i) * cardWPaddingWidth + val bcRight = centerX - (boardCards / 2f - i) * cardWPaddingWidth + cardWidth + this.boardCardRects.add(Rect(bcLeft, bcTop, bcRight, bcBottom)) + } + + val chipTextSize = this.chipRadius + this.potChipCircle = Circle(centerX, bcTop - 3.7f * chipTextSize, this.chipRadius) + this.potTextPoint = TextPoint(centerX, bcTop - 1.6f * chipTextSize, chipTextSize) + this.totalPotTextPoint = TextPoint(centerX, bcTop - 0.5f * chipTextSize, chipTextSize) + + val positions = MathUtils.positionsAroundRoundedRectangle(this.players, this.tableRect, this.tableCornerXRadius, this.tableCornerYRadius) + for (i in (0 until this.players)) { + + val point = positions[i].first + val bottomOriented = positions[i].second + if (i == positions.size - 1) { dealerBottomOriented = bottomOriented } + + val rectCenterX = point.x + val rectCenterY = point.y + + val left = rectCenterX - this.playerItemsWidth / 2 + val top = rectCenterY - this.playerItemsHeight / 2 + val right = rectCenterX + this.playerItemsWidth / 2 + val bottom = rectCenterY + this.playerItemsHeight / 2 + val pRect = Rect(left, top, right, bottom) + this.playerStackRects.add(pRect) + + val line = MathUtils.Line(point.x, point.y, tableRect.centerX, tableRect.centerY) + val prp = line.intersects(pRect) ?: throw PAException("should not happen") + + val boxToCenterLine = MathUtils.Line(prp.x, prp.y, tableRect.centerX, tableRect.centerY) + val boxToChipDistance = if (bottomOriented) 3f * chipRadius else 2f * chipRadius // because the chip text needs space + val chipPoint = boxToCenterLine.pointForDistance(boxToChipDistance) + + this.chipCircles.add(Circle(chipPoint.x, chipPoint.y, chipRadius)) + + this.chipTextPoints.add(TextPoint(chipPoint.x, chipPoint.y + 2 * chipTextSize, chipTextSize)) + + // we give each text zone 1/3rd of the box height, leaving 1/3 for space + // the y given is the bottom of the text rect, giving 1/18th as the offset + // 1 / (3_total_space * 3_each_space * 2_center) + + val bottomOffset = this.playerItemsHeight / (3 * 3 * 2) + val fontSize = this.playerItemsHeight / 3 + this.playerNamePoints.add(TextPoint(rectCenterX, rectCenterY - bottomOffset, fontSize)) + this.playerStackPoints.add(TextPoint(rectCenterX, rectCenterY + this.playerItemsHeight / 3, fontSize)) + this.playerActionPoints.add(TextPoint(rectCenterX, rectCenterY + this.playerItemsHeight / 9, fontSize)) + + val orientation = if (bottomOriented) -1 else 1 + val circleOffset = playerItemsHeight * 1.75f * orientation + this.playerCircles.add(Circle(rectCenterX, rectCenterY - circleOffset, this.playerItemsHeight / 2)) + + val cardsUsed = this.handHistory.cardsCountForPlayer(i) ?: maxPlayerCards + val cardsRectangles = mutableListOf() + if (cardsUsed > 0) { + val offSet = (cardsUsed / 2 - 0.5f) * cardWPaddingWidth + + val cardCenterY = rectCenterY - circleOffset / 2 + for (c in 0 until cardsUsed) { + + val cardCenterX = rectCenterX - offSet + c * cardWPaddingWidth + val cardRect = Rect(cardCenterX - cardSpecs.width / 2, cardCenterY - cardSpecs.height / 2, cardCenterX + cardSpecs.width / 2, cardCenterY + cardSpecs.height / 2) + cardsRectangles.add(cardRect) + } + } + this.playerCardRects.add(cardsRectangles) + } + + } + +} \ No newline at end of file diff --git a/kmmshared/src/iosMain/kotlin/com/staxriver/kmmshared/Platform.kt b/kmmshared/src/iosMain/kotlin/com/staxriver/kmmshared/Platform.kt new file mode 100644 index 00000000..ca37e247 --- /dev/null +++ b/kmmshared/src/iosMain/kotlin/com/staxriver/kmmshared/Platform.kt @@ -0,0 +1,8 @@ +package com.staxriver.kmmshared + +import platform.UIKit.UIDevice + +actual class Platform actual constructor() { + actual val platform: String = + UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e7b4def4..fefa5156 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +include ':kmmshared' include ':app'