Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
710dbc2530 | 5 years ago |
@ -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") |
||||||
|
} |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<manifest package="com.staxriver.kmmshared" /> |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
package com.staxriver.kmmshared |
||||||
|
|
||||||
|
actual class Platform actual constructor() { |
||||||
|
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" |
||||||
|
} |
||||||
@ -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 } |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
package com.staxriver.kmmshared |
||||||
|
|
||||||
|
class Greeting { |
||||||
|
fun greeting(): String { |
||||||
|
return "Hello, ${Platform().platform}!" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package com.staxriver.kmmshared |
||||||
|
|
||||||
|
interface HandHistoryInformer { |
||||||
|
|
||||||
|
val playerCount: Int |
||||||
|
val maxCards: Int |
||||||
|
fun cardsCountForPlayer(position: Int): Int? |
||||||
|
|
||||||
|
} |
||||||
@ -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<Pair<Point, Boolean>> { |
||||||
|
|
||||||
|
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<Pair<Point, Boolean>>() |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
package com.staxriver.kmmshared |
||||||
|
|
||||||
|
class PAException(message: String) : Exception(message) |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
package com.staxriver.kmmshared |
||||||
|
|
||||||
|
expect class Platform() { |
||||||
|
val platform: String |
||||||
|
} |
||||||
@ -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<Rect>() |
||||||
|
private var playerCircles = mutableListOf<Circle>() |
||||||
|
private var playerNamePoints = mutableListOf<TextPoint>() |
||||||
|
private var playerStackPoints = mutableListOf<TextPoint>() |
||||||
|
private var playerActionPoints = mutableListOf<TextPoint>() |
||||||
|
private var playerCardRects = mutableListOf<List<Rect>>() |
||||||
|
|
||||||
|
private var chipCircles = mutableListOf<Circle>() |
||||||
|
private var chipTextPoints = mutableListOf<TextPoint>() |
||||||
|
|
||||||
|
var boardCardRects = mutableListOf<Rect>() |
||||||
|
|
||||||
|
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<Rect>() |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -1 +1,2 @@ |
|||||||
|
include ':kmmshared' |
||||||
include ':app' |
include ':app' |
||||||
|
|||||||
Loading…
Reference in new issue