├── .env ├── .gitignore ├── .gitmodules ├── DB.md ├── LICENSE.md ├── NOTATION.md ├── Procfile ├── README.md ├── RULES.md ├── TODO.md ├── app.json ├── backend ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── penta │ │ │ └── server │ │ │ ├── AuthedSessionEvent.kt │ │ │ ├── GameController.kt │ │ │ ├── GlobalState.kt │ │ │ ├── LobbyHandler.kt │ │ │ ├── Main.kt │ │ │ ├── PentaApp.kt │ │ │ ├── Routes.kt │ │ │ ├── ServerGamestate.kt │ │ │ ├── ServerUserInfo.kt │ │ │ ├── SessionController.kt │ │ │ ├── SessionState.kt │ │ │ ├── StatusPages.kt │ │ │ ├── StoreExt.kt │ │ │ ├── User.kt │ │ │ ├── UserManager.kt │ │ │ ├── UserSession.kt │ │ │ ├── db │ │ │ ├── DB.kt │ │ │ ├── Games.kt │ │ │ ├── Users.kt │ │ │ └── jsonb.kt │ │ │ └── feature │ │ │ └── EncryptionEnforcementFeature.kt │ └── resources │ │ ├── application.conf │ │ ├── db │ │ └── migration │ │ │ ├── V1.sql │ │ │ └── V2.sql │ │ └── logback.xml │ └── test │ └── kotlin │ ├── tests │ ├── DBTests.kt │ └── Tests.kt │ └── util │ └── ResetDB.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── ConstantsGenerator.kt │ ├── Versions.kt │ ├── client.kt │ └── org │ └── gradle │ └── kotlin │ └── dsl │ └── Extensions.kt ├── frontend ├── build.gradle.kts ├── src │ └── main │ │ ├── kotlin │ │ ├── Index.kt │ │ ├── components │ │ │ ├── App.kt │ │ │ ├── GameSetupControls.kt │ │ │ ├── PentaSvg.kt │ │ │ ├── TextBoard.kt │ │ │ └── TextConnection.kt │ │ ├── externals │ │ │ ├── redux-logger.kt │ │ │ ├── redux-logger_aliases.kt │ │ │ └── redux-logger_impls.kt │ │ ├── reducers │ │ │ └── Reducers.kt │ │ └── util │ │ │ ├── CanvasExtensions.kt │ │ │ ├── DrawPentagame.kt │ │ │ ├── ReactExt.kt │ │ │ └── Util.kt │ │ └── resources │ │ ├── favicon.ico │ │ ├── float.css │ │ ├── index.html │ │ └── manifest.json └── webpack.config.d │ └── webpack.config.js ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── packages.json ├── settings.gradle.kts ├── shared ├── build.gradle.kts └── src │ ├── commonClient │ └── kotlin │ │ ├── client.kt │ │ └── penta │ │ ├── ClientUtil.kt │ │ ├── ConnectionState.kt │ │ ├── PentagameClick.kt │ │ ├── WSClient.kt │ │ ├── redux │ │ └── MultiplayerState.kt │ │ ├── test.kt │ │ └── util │ │ └── HttpExt.kt │ ├── commonMain │ ├── kotlin │ │ ├── CharUtil.kt │ │ ├── actions │ │ │ └── Actions.kt │ │ └── penta │ │ │ ├── BoardState.kt │ │ │ ├── IllegalMoveException.kt │ │ │ ├── LobbyState.kt │ │ │ ├── PentaBoard.kt │ │ │ ├── PentaColor.kt │ │ │ ├── PentaMath.kt │ │ │ ├── PentaMove.kt │ │ │ ├── PlayerFaces.kt │ │ │ ├── PlayerIds.kt │ │ │ ├── UserInfo.kt │ │ │ ├── logic │ │ │ ├── Field.kt │ │ │ ├── GameType.kt │ │ │ └── Piece.kt │ │ │ ├── network │ │ │ ├── GameEvent.kt │ │ │ ├── GameSessionInfo.kt │ │ │ ├── LobbyEvent.kt │ │ │ ├── LoginRequest.kt │ │ │ ├── LoginResponse.kt │ │ │ ├── ServerStatus.kt │ │ │ └── SessionEvent.kt │ │ │ └── util │ │ │ ├── ColorUtil.kt │ │ │ ├── ExceptionHandler.kt │ │ │ ├── ExhaustiveChecking.kt │ │ │ ├── Json.kt │ │ │ ├── ListExtension.kt │ │ │ ├── LoggerExtension.kt │ │ │ ├── ObjectSerializer.kt │ │ │ └── PointExtensions.kt │ └── resources │ │ └── logback.xml │ ├── jsMain │ └── kotlin │ │ ├── ClientJs.kt │ │ ├── ConsoleExt.kt │ │ ├── WrapperStore.kt │ │ ├── actions │ │ └── Actions.kt │ │ ├── isUpperCase.kt │ │ └── launch.kt │ └── jvmMain │ └── kotlin │ ├── actions │ └── Actions.kt │ ├── isUpperCase.kt │ └── penta │ └── util │ └── Middlewares.kt ├── system.properties ├── upload.sh └── versions.properties /.env: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | JDBC_DATABASE_URL=jdbc:postgresql://localhost:5432/pentagame?user=postgres 3 | CHROME_BIN=chromium 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | 6 | # idea 7 | 8 | .idea/ 9 | !.idea/runConfigurations/* 10 | 11 | out/ 12 | *.iml 13 | *.ipr 14 | *.iws 15 | 16 | node_modules/ 17 | package-lock.json 18 | 19 | private.gradle.kts 20 | run/ 21 | *.jar 22 | !/gradle/wrapper/*.jar 23 | 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "muirwik"] 2 | path = muirwik 3 | url = https://github.com/NikkyAI/muirwik.git -------------------------------------------------------------------------------- /DB.md: -------------------------------------------------------------------------------- 1 | # migration testing 2 | 3 | 1. copy heroki/live db to backup_v_$(version) \ 4 | ``` 5 | pg_dump -C -h $DB_SERVER -U postgres pentagame -f "backup_v$(VERSION).sql" 6 | psql -U username -c "CREATE DATABASE "backup_v$(VERSION)" 7 | psql -U postgres -d "backup_v$(VERSION)" -f "backup_v$(VERSION).sql" 8 | ``` 9 | 10 | 2. copy to `test_v$(version)` \ 11 | ```sql 12 | DROP DATABASE pentagame_test_v$(VERSION); 13 | CREATE DATABASE pentagame_test_v$(VERSION) 14 | WITH TEMPLATE backup_v$(VERSION); 15 | ``` 16 | 17 | 3. run migration code on `test_v$(VERSION)` \ 18 | ``` 19 | ./gradlew flyway 20 | ``` 21 | 22 | 23 | 2. run db_test on `test_v$(VERSION)` \ 24 | ```bash 25 | ./gradlew :backend:jvmTests 26 | ``` 27 | 28 | # creating migration 29 | 30 | https://www.apgdiff.com/ 31 | 32 | 1. copy heroki/live db to backup_v_$(version) \ 33 | ``` 34 | pg_dump -C -h $DB_SERVER -U postgres pentagame -f "backup_v$(VERSION).sql" 35 | ``` 36 | 37 | 2 dump current and create new empty database \ 38 | ```shell script 39 | DROP DATABASE pentagame_new; 40 | CREATE DATABASE pentagame_new; 41 | ./gradlew :backend:test 42 | 43 | pg_dump -C -h localhost -U postgres pentagame_new -f "pentagame_new.sql" 44 | ``` 45 | 46 | 3. run diff as gradle task \ 47 | ```shell script 48 | # java -jar apgdiff-2.4.jar --ignore-start-with original.sql new.sql > upgrade.sql 49 | java -jar apgdiff-2.4.jar --ignore-start-with backup_v$(VERSION).sql pentagame_new.sql > upgrade.sql 50 | migra $(backup_v$(version) psql://localhost/dev_db 51 | ``` 52 | 53 | 54 | # Notes: 55 | 56 | `choco install postgresql11 /Password:postgres` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 NikkyAI 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /NOTATION.md: -------------------------------------------------------------------------------- 1 | 2 | symbols | alt | meaning 3 | --------|-------|------- 4 | A, B, . . . | | players 5 | A, B, C, D, E | | outer nodes (corners) 6 | a, b, c, d, e | | inner nodes (goals, crossings, junctions) 7 | a, b, c, d, e | | pieces 8 | → | --> | move without swap 9 | ↔ | <-> | swap own pieces 10 | ⇆ | <+> | swap foreign piece 11 | ⇔ | <=> | cooperative swap (4-player) 12 | × | x| replacing a block 13 | (. . .) | | a move (of a piece) 14 | [. . .] | | block placement 15 | + | + | reaching a goal 16 | ! | | recommendable 17 | ? | | questionable 18 | 4 | | idea 19 | 20 | 21 | → ↔ ⇆ ⇔ 22 | 23 | 24 | 25 | ## Movements 26 | 27 | a (A -> B) 28 | piece `a` goes from `A` to `B` 29 | 30 | e(A -> B-4-e) 31 | piece `e` goes from `A` to between `B` and `e` 32 | 33 | ## Swaps 34 | 35 | a, b (A <-> B) 36 | swaps pieces `a` and `b` which were on `A` and `B` 37 | 38 | 39 | ## replacing a black block and placing black 40 | 41 | d (D → b) & [E-1-b] 42 | piece `d` moves from `D` to `b` and black piece is set between `E` and `b` 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -jar backend/build/libs/backend-all.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pentagame 2 | 3 | github: [htps://github.com/NikkyAi/pentagame](htps://github.com/NikkyAi/pentagame) 4 | 5 | online: [demo](https://pentagame.herokuapp.com/) 6 | 7 | ## Building 8 | 9 | ### Backend 10 | 11 | build: `./gradlew :backend:shadowJar` 12 | execute: `heroku local:start` 13 | 14 | find runnable jar in `backend/build/libs/`, 15 | you may need to set some environment manually 16 | 17 | ### Frontend 18 | 19 | js is bundled in the server automatically 20 | 21 | for dev: 22 | `./gradlew :frontend:run -t` 23 | alternative: 24 | `./gradlew :frontend:bundle` 25 | then open `frontend/build/bundle/index.html` in a browser (using idea as webserver) 26 | 27 | ### Database 28 | 29 | launch a local postgres container with 30 | ```sh 31 | docker pull postgres 32 | docker run --rm --name pg-docker -e POSTGRES_PASSWORD=docker -d -p 5432:5432 -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data postgres 33 | ``` 34 | 35 | https://hackernoon.com/dont-install-postgres-docker-pull-postgres-bee20e200198 36 | 37 | ### Recording a game 38 | 39 | The game is currently recorded by the getClient and server, but not serialized or stored 40 | 41 | There is currently more data recorded than strictly necessary too 42 | 43 | TODO 44 | 45 | To record a game some initial setup is necessary, 46 | knowing the amount of players and which team they are in 47 | 48 | 49 | # TODO 50 | 51 | players have 4 ranked chosen symbols 52 | the highest ranking player gets their preferred symbol in case of conflict of choice 53 | 54 | # UI 55 | 56 | vertical or curved on corner: 57 | - player names/faces 58 | highlight this turn 59 | digital clock 60 | 61 | tabs: 62 | - help & rules 63 | - multiplayer/login: 64 | - server / connection info 65 | - all players info 66 | - session chat 67 | - rooms / games 68 | - global chat ? 69 | - account info 70 | - notation 71 | - debug export 72 | 73 | 74 | # ELO 75 | 76 | new players have `null` ranking 77 | when a new player beats a ranked player 78 | they get awarded the same rank points 79 | then points get redistributed 80 | total rank points increase with total ranked player count 81 | separated rankings for 2, 3, 4, 2v2 players -------------------------------------------------------------------------------- /RULES.md: -------------------------------------------------------------------------------- 1 | # Rules 2 | 3 | globalState: 4 | - board 5 | - turn 6 | - players 7 | 8 | - start(currentPlayer) 9 | - canClickOnPiece(targetPiece) 10 | - piece.player == currentPlayer 11 | -> actions: selectPlayerTarget(currentPlayerPiece = piece) 12 | 13 | - selectPlayerTarget(playerPiece: Piece) 14 | - canClickOnPiece(targetPiece: Piece) 15 | - hasPath 16 | && targetPiece.field != playerPiece 17 | && targetPiece.type == player 18 | -> actions: movePiece(targetPiece, playerPiece.field) 19 | -> actions: movePlayerPiece(playerPiece, playerPiece.field) 20 | - hasPath 21 | && targetPiece.field != playerPiece 22 | && targetPiece.type == black 23 | -> actions: movePlayerPiece(playerPiece, targetPiece.field) 24 | -> actions: placeBlack(targetPiece) 25 | - hasPath 26 | && targetPiece.field != playerPiece 27 | && targetPiece.type == gray 28 | -> actions: movePlayerPiece(playerPiece, targetPiece.field) 29 | -> actions: removePiece(targetPiece) 30 | - canClickOnField(targetField) 31 | - hasPath 32 | && targetField.isEmpty 33 | -> actions: movePlayerPiece(playerPiece, targetField) 34 | 35 | - movePlayerPiece(playerPiece: Piece, targetField; Field) 36 | - targetField.type == joint 37 | -> actions: removePiece(playerPiece) 38 | -> actions: pickGray() 39 | - else 40 | -> actions: movePiece(playerPiece, targetField) 41 | 42 | - placeBlack(blackPiece: Piece) 43 | - canClickOnField(targetField: Field) 44 | - targetField.isEmpty 45 | -> actions: movePiece(blackPiece, targetField) 46 | 47 | - pickGray 48 | - val grayPiece = board.findPiece(field = null, type = gray) 49 | -> actions: return placeGray(grayPiece) 50 | - canClickOnPiece(targetPiece: Piece) 51 | - targetPiece.type == gray 52 | // && board.pieces.byType(gray).length 53 | -> actions: return placeGray(targetPiece) 54 | 55 | - placeGray(grayPiece: Piece) 56 | - canClickOnField(targetField: Field) 57 | - targetField.isEmpty 58 | -> actions: movePiece(grayPiece, targetField) 59 | 60 | - hasPath(start: Field, end: Field) # builtin? 61 | - checkConnection(start, end, [start]) 62 | 63 | - checkConnection(field: Field, target: Field, skip: Field[]): Boolean 64 | - forEach field.connected 65 | - if field in skip 66 | - `continue@forEach` 67 | - -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | RefreshVersions 4 | 5 | - migrate versions to "_" and hope for the best 6 | - run gradle refreshVersions 7 | - verify plugin versions before https://scans.gradle.com/s/q7sfru33i4fxs/plugins?expanded=WzM4XQ 8 | - verify dependencies version before migration https://scans.gradle.com/s/q7sfru33i4fxs/dependencies 9 | 10 | 11 | - Bugs \ 12 | see [issues](https://github.com/NikkyAI/pentagame/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 13 | 14 | - Frontend 15 | - Redux/Statemachine 16 | - [x] process `LobbyEvents` 17 | - UI 18 | - [ ] track points for players 19 | - [ ] change figure shapes for players 20 | 21 | - [ ] Notation Parser 22 | - [ ] finalize notation syntax 23 | 24 | - [ ] User 25 | - [ ] Backend 26 | - [ ] add User storage (Postgres + Exposed) 27 | - [ ] add routes for setting user data (password, displayname) 28 | - [ ] Frontend 29 | - [ ] add views 30 | 31 | - General 32 | - [ ] add logic and UI for 2v2 and other team modes 33 | - [ ] Database 34 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pentagame", 3 | "description": "The Pentagram Shaped Board Game, deployed to Heroku.", 4 | "addons": [ ], 5 | "buildpacks": [ 6 | { 7 | "url": "heroku/nodejs" 8 | }, 9 | { 10 | "url": "heroku/gradle" 11 | } 12 | ], 13 | "success_url": "/", 14 | "website": "http://pentagame.org/" 15 | } -------------------------------------------------------------------------------- /backend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | import java.io.ByteArrayOutputStream 3 | import java.io.PrintStream 4 | 5 | 6 | plugins { 7 | kotlin("jvm") 8 | id("com.github.johnrengelman.shadow") version "5.0.0" 9 | id("org.flywaydb.flyway") version Flyway.version 10 | application 11 | id("de.fayard.dependencies") 12 | } 13 | 14 | val gen_resource = buildDir.resolve("gen-src/resources").apply { mkdirs() } 15 | 16 | val hasDevUrl = extra.has("DEV_JDBC_DATABASE_URL") 17 | if (!hasDevUrl) logger.error("DEV_JDBC_DATABASE_URL not set") 18 | val hasLiveUrl = extra.has("LIVE_JDBC_DATABASE_URL") 19 | if (!hasLiveUrl) logger.error("LIVE_JDBC_DATABASE_URL not set") 20 | 21 | val flywayUrl = System.getenv()["JDBC_DATABASE_URL"] ?: if (hasDevUrl) { 22 | val DEV_JDBC_DATABASE_URL: String by extra 23 | DEV_JDBC_DATABASE_URL 24 | } else { 25 | null 26 | } 27 | if(flywayUrl != null) { 28 | // val jdbc = split_jdbc(flywayUrl) 29 | flyway { 30 | url = flywayUrl 31 | schemas = arrayOf("public") 32 | baselineVersion = "0" 33 | } 34 | } 35 | if (hasDevUrl) { 36 | // tasks.register("resetDatabase") { 37 | // group = "database" 38 | // 39 | // doFirst { 40 | // 41 | // } 42 | // this.finalizedBy("") 43 | // 44 | // } 45 | } 46 | if (hasDevUrl && hasLiveUrl) { 47 | val DEV_JDBC_DATABASE_URL: String by extra 48 | val LIVE_JDBC_DATABASE_URL: String by extra 49 | 50 | tasks.register("createMigration") { 51 | group = "database" 52 | 53 | dependsOn("testClasses") 54 | 55 | val dumps = buildDir.resolve("dumps") 56 | 57 | doFirst { 58 | dumps.mkdirs() 59 | val livePath = dumps.resolve("live_schema.sql").path 60 | val devPath = dumps.resolve("dev_schema.sql").path 61 | 62 | pg_dump( 63 | connectionString = LIVE_JDBC_DATABASE_URL, 64 | target = livePath, 65 | extraArgs = arrayOf( 66 | "--create", 67 | "--no-owner", 68 | "--no-acl", 69 | "--exclude-table", "flyway_schema_history" 70 | /*, "--schema-only"*/) 71 | ) 72 | 73 | // drop and regenerate dev database 74 | javaexec { 75 | main = "util.ResetDB" 76 | classpath = sourceSets.test.get().runtimeClasspath 77 | environment("JDBC_DATABASE_URL", DEV_JDBC_DATABASE_URL) 78 | } 79 | 80 | pg_dump( 81 | connectionString = DEV_JDBC_DATABASE_URL, 82 | target = devPath, 83 | extraArgs = arrayOf( 84 | "--create", 85 | "--no-owner", 86 | "--no-acl", 87 | "--exclude-table", "flyway_schema_history" 88 | ) 89 | ) 90 | 91 | val old = System.out 92 | val migrationStatements = ByteArrayOutputStream().use { os -> 93 | System.setOut(PrintStream(os)) 94 | 95 | cz.startnet.utils.pgdiff.Main.main( 96 | arrayOf( 97 | "--ignore-start-with", 98 | livePath, 99 | devPath 100 | ) 101 | ) 102 | os.toString() 103 | } 104 | System.setOut(old) 105 | 106 | // val migrationStatements = ByteArrayOutputStream().use { os -> 107 | // javaexec { 108 | // main = "cz.startnet.utils.pgdiff.Main" 109 | // classpath = files("apgdiff-2.4.jar") 110 | // args = listOf("--ignore-start-with", dumps.resolve("v0.sql").path, dumps.resolve("v1.sql").path) 111 | // standardOutput = os 112 | // } 113 | // os.toString() 114 | // } 115 | 116 | logger.lifecycle("migration statements \n$migrationStatements") 117 | 118 | file("src/main/resources/db/migration/next.sql") 119 | .writeText(migrationStatements) 120 | // TODO: write statements to file 121 | } 122 | } 123 | } 124 | 125 | dependencies { 126 | implementation(kotlin("stdlib-jdk8")) 127 | 128 | implementation(project(":shared")) 129 | 130 | implementation("io.ktor:ktor-server-netty:${Ktor.version}") 131 | implementation("io.ktor:ktor-html-builder:${Ktor.version}") 132 | implementation("io.ktor:ktor-serialization:${Ktor.version}") 133 | implementation("ch.qos.logback:logback-classic:${Logback.version}") 134 | 135 | implementation("org.postgresql:postgresql:${Postgres.version}") 136 | 137 | implementation("org.jetbrains.exposed:exposed-core:${Exposed.version}") 138 | implementation("org.jetbrains.exposed:exposed-dao:${Exposed.version}") 139 | implementation("org.jetbrains.exposed:exposed-jdbc:${Exposed.version}") 140 | 141 | // implementation ("com.improve_future:harmonica:1.1.24") 142 | // implementation (group = "org.reflections", name= "reflections", version= "0.9.11") 143 | 144 | testImplementation(kotlin("test-junit")) 145 | } 146 | 147 | kotlin { 148 | target { 149 | compilations.all { 150 | kotlinOptions { 151 | jvmTarget = "1.8" 152 | } 153 | } 154 | } 155 | } 156 | tasks { 157 | compileKotlin { 158 | kotlinOptions.jvmTarget = "1.8" 159 | } 160 | compileTestKotlin { 161 | kotlinOptions.jvmTarget = "1.8" 162 | } 163 | } 164 | 165 | val packageStatic = tasks.create("packageStatic") { 166 | group = "build" 167 | val frontend = project(":frontend") 168 | dependsOn(":frontend:processResources") 169 | if(properties.contains("dev")) { 170 | dependsOn(":frontend:browserDevelopmentWebpack") 171 | } else { 172 | dependsOn(":frontend:browserProductionWebpack") 173 | } 174 | 175 | outputs.upToDateWhen { false } 176 | outputs.dir(gen_resource) 177 | 178 | val staticFolder = gen_resource.resolve("static").apply { mkdirs() } 179 | 180 | doFirst { 181 | staticFolder.deleteRecursively() 182 | staticFolder.mkdirs() 183 | copy { 184 | from(frontend.tasks.get("processResources")) 185 | from(frontend.buildDir.resolve("distributions")) 186 | into(staticFolder) 187 | } 188 | } 189 | 190 | // from(terserTask) 191 | // from(bundleTask) 192 | 193 | doLast { 194 | // copy { 195 | // from(frontend.buildDir.resolve("distributions")) 196 | // into(staticFolder.resolve("js")) 197 | // } 198 | } 199 | } 200 | 201 | application { 202 | mainClassName = "io.ktor.server.cio.EngineMain" 203 | } 204 | 205 | val shadowJar = tasks.getByName("shadowJar") { 206 | dependsOn(packageStatic) 207 | 208 | from(packageStatic) 209 | } 210 | 211 | val unzipJsJar = tasks.create("unzipShadowJar") { 212 | dependsOn(shadowJar) 213 | group = "build" 214 | from(zipTree(shadowJar.archiveFile)) 215 | into(shadowJar.destinationDirectory.file(shadowJar.archiveBaseName)) 216 | } 217 | 218 | task("depsize") { 219 | group = "help" 220 | description = "prints dependency sizes" 221 | doLast { 222 | val formatStr = "%,10.2f" 223 | val configuration = kotlin.target.compilations.getByName("main").compileDependencyFiles as Configuration 224 | val size = configuration.resolve() 225 | .map { it.length() / (1024.0 * 1024.0) }.sum() 226 | 227 | val out = buildString { 228 | append("Total dependencies size:".padEnd(55)) 229 | append("${String.format(formatStr, size)} Mb\n\n") 230 | configuration 231 | .resolve() 232 | .sortedWith(compareBy { -it.length() }) 233 | .forEach { 234 | append(it.name.padEnd(55)) 235 | append("${String.format(formatStr, (it.length() / 1024.0))} kb\n") 236 | } 237 | } 238 | println(out) 239 | } 240 | } 241 | 242 | //val run = tasks.getByName("run") { 243 | // dependsOn(shadowJar) 244 | //} 245 | 246 | //tasks.register("run") { 247 | // dependsOn("jar") 248 | // 249 | // main = "com.bdudelsack.fullstack.ApplicationKt" 250 | // 251 | // classpath(configurations["runtimeClasspath"].resolvedConfiguration.files,jar.archiveFile.get().toString()) 252 | // args = listOf() 253 | //} 254 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/AuthedSessionEvent.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import SessionEvent 4 | 5 | data class AuthedSessionEvent( 6 | val event: SessionEvent, 7 | val user: User 8 | ) { 9 | 10 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/GameController.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import com.soywiz.klogger.Logger 4 | 5 | object GameController { 6 | private val logger = Logger(this::class.simpleName!!) 7 | var idCounter = 0 8 | 9 | suspend fun create(owner: User): ServerGamestate { 10 | logger.info { "creating game for $owner" } 11 | val game = ServerGamestate("game_${idCounter++}", owner) 12 | GlobalState.dispatch(GlobalState.GlobalAction.AddGame(game)) 13 | return game 14 | } 15 | 16 | suspend fun get(gameId: String): ServerGamestate? { 17 | return GlobalState.getState().games.find { it.serverGameId == gameId } 18 | } 19 | 20 | suspend fun listActiveGames() = GlobalState.getState().games.filter { 21 | it.getBoardState().winner == null 22 | } 23 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/GlobalState.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import com.soywiz.klogger.Logger 4 | import io.ktor.http.cio.websocket.Frame 5 | import io.ktor.websocket.DefaultWebSocketServerSession 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.runBlocking 9 | import kotlinx.serialization.builtins.list 10 | import kotlinx.serialization.list 11 | import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger 12 | import org.jetbrains.exposed.sql.addLogger 13 | import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction 14 | import org.jetbrains.exposed.sql.transactions.transaction 15 | import penta.LobbyState 16 | import penta.network.GameEvent 17 | import penta.network.LobbyEvent 18 | import penta.server.db.Game 19 | import penta.util.handler 20 | import penta.util.json 21 | 22 | data class GlobalState( 23 | val games: List = loadGames(), 24 | 25 | // lobby 26 | val observingSessions: Map = mapOf(), 27 | val lobbyState: LobbyState = LobbyState() 28 | ) { 29 | fun reduce(action: Any): GlobalState = when (action) { 30 | is GlobalAction -> when (action) { 31 | is GlobalAction.AddSession -> { 32 | GlobalScope.launch(handler) { 33 | observingSessions.forEach { (session, wss) -> 34 | if (wss != action.websocketSession) { 35 | wss.outgoing.send( 36 | Frame.Text( 37 | json.stringify( 38 | LobbyEvent.serializer(), 39 | LobbyEvent.Join(action.session.userId) 40 | ) 41 | ) 42 | ) 43 | } 44 | } 45 | } 46 | copy( 47 | observingSessions = observingSessions + (action.session to action.websocketSession) 48 | ) 49 | } 50 | is GlobalAction.RemoveSession -> { 51 | val toRemove = observingSessions[action.session] 52 | GlobalScope.launch(handler) { 53 | observingSessions.forEach { (session, wss) -> 54 | if (wss != toRemove) { 55 | wss.outgoing.send( 56 | Frame.Text( 57 | json.stringify(LobbyEvent.serializer(), LobbyEvent.Leave(action.session.userId, "")) 58 | ) 59 | ) 60 | } 61 | } 62 | } 63 | copy( 64 | observingSessions = observingSessions - action.session 65 | ) 66 | } 67 | is GlobalAction.AddGame -> { 68 | val boardState = runBlocking { action.game.getBoardState() } 69 | val game = transaction { 70 | addLogger(Slf4jSqlDebugLogger) 71 | Game.new { 72 | gameId = action.game.serverGameId 73 | history = json.stringify( 74 | GameEvent.serializer().list, 75 | boardState.history.map { it.toSerializable() } 76 | ) 77 | owner = UserManager.toDBUser(action.game.owner) 78 | } 79 | } 80 | // TODO: deal with users in game ? 81 | // TODO: get sessionState here 82 | // transaction { 83 | // addLogger(Slf4jSqlDebugLogger) 84 | // game.players = SizedCollection( 85 | // boardState.players.mapNotNull { 86 | // UserManager.findDBUser(it.id) 87 | // } 88 | // ) 89 | // } 90 | copy(games = games + action.game) 91 | .reduce(LobbyEvent.UpdateGame(action.game.info)) 92 | } 93 | } 94 | is LobbyEvent -> { 95 | GlobalScope.launch(handler) { 96 | observingSessions.forEach { (session, ws) -> 97 | ws.send( 98 | Frame.Text(json.stringify(LobbyEvent.serializer(), action)) 99 | ) 100 | } 101 | } 102 | 103 | copy( 104 | lobbyState = lobbyState.reduce(action) 105 | ) 106 | } 107 | else -> this 108 | } 109 | 110 | sealed class GlobalAction { 111 | data class AddSession( 112 | val session: UserSession, 113 | val websocketSession: DefaultWebSocketServerSession 114 | ) : GlobalAction() 115 | 116 | data class RemoveSession( 117 | val session: UserSession 118 | ) : GlobalAction() 119 | 120 | data class AddGame( 121 | val game: ServerGamestate 122 | ) : GlobalAction() 123 | } 124 | 125 | companion object { 126 | private val logger = Logger(this::class.simpleName!!) 127 | // private val context = newSingleThreadContext("store") 128 | // private val store = runBlocking(context) { 129 | // sameThreadEnforcementWrapper( 130 | // createStore( 131 | // GlobalState::reduce, 132 | // GlobalState(), 133 | // applyMiddleware(loggingMiddleware(GlobalState.logger)) 134 | // ), 135 | // context 136 | // ) 137 | // } 138 | // suspend fun getState() = withContext(context) { 139 | // store.state 140 | // } 141 | // suspend fun dispatch(action: Any) = withContext(context) { 142 | // store.dispatch(action) 143 | // } 144 | private val lock = object {} 145 | 146 | private var state = GlobalState() 147 | fun getState(): GlobalState { 148 | synchronized(lock) { 149 | return state 150 | } 151 | } 152 | fun dispatch(action: Any) { 153 | synchronized(lock) { 154 | state = state.reduce(action) 155 | } 156 | } 157 | 158 | fun loadGames(): List = runBlocking { 159 | newSuspendedTransaction { 160 | Game.all().map { game -> 161 | GameController.idCounter++ 162 | ServerGamestate( 163 | game.gameId, 164 | game.owner.let { u -> 165 | UserManager.convert(u) 166 | } 167 | ).apply { 168 | // load playing users 169 | loadUsers(game) 170 | // apply all history 171 | json.parse(GameEvent.serializer().list, game.history).forEach { move -> 172 | boardDispatch(move) 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/LobbyHandler.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import io.ktor.http.cio.websocket.CloseReason 4 | import io.ktor.http.cio.websocket.Frame 5 | import io.ktor.http.cio.websocket.close 6 | import io.ktor.http.cio.websocket.readText 7 | import io.ktor.websocket.DefaultWebSocketServerSession 8 | import kotlinx.coroutines.channels.ClosedReceiveChannelException 9 | import kotlinx.coroutines.channels.ClosedSendChannelException 10 | import mu.KotlinLogging 11 | import penta.network.LobbyEvent 12 | import penta.util.exhaustive 13 | import penta.util.json 14 | import java.io.IOException 15 | 16 | object LobbyHandler { 17 | private val logger = KotlinLogging.logger {} 18 | private val observingSessions: MutableMap = mutableMapOf() 19 | 20 | private val serializer = LobbyEvent.serializer() 21 | suspend fun handle(websocketSession: DefaultWebSocketServerSession, session: UserSession) = with(websocketSession) { 22 | try { 23 | // play back history 24 | logger.info { "sending observers" } 25 | logger.info { "sending chat history" } 26 | logger.info { "sending game list" } 27 | val state = GlobalState.getState() 28 | outgoing.send( 29 | Frame.Text( 30 | json.stringify( 31 | serializer, 32 | LobbyEvent.InitialSync( 33 | users = state.observingSessions.keys.map { it.userId }, 34 | chat = state.lobbyState.chat.take(50), 35 | games = GameController.listActiveGames() 36 | .associate { gameState -> 37 | gameState.serverGameId to gameState.info 38 | } 39 | ) 40 | ) 41 | ) 42 | ) 43 | 44 | // register as observer 45 | 46 | // observeringSessions[session] = this 47 | 48 | // TODO: also send initial sync through action ? 49 | GlobalState.dispatch(GlobalState.GlobalAction.AddSession(session, this)) 50 | 51 | while (true) { 52 | val notationJson = (incoming.receive() as Frame.Text).readText() 53 | logger.info { "ws received: $notationJson" } 54 | 55 | val event = json.parse(serializer, notationJson) as? LobbyEvent.FromClient ?: run { 56 | close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "event: $this cannot be sent by client")) 57 | return 58 | } 59 | when (event) { 60 | is LobbyEvent.Message -> { 61 | if (session.userId == event.userId) { 62 | GlobalState.dispatch(event) 63 | // chatHistoryAdd(event) 64 | } else { 65 | logger.error { "user ${session.userId} sent message from ${event.userId}" } 66 | } 67 | } 68 | else -> { 69 | TODO("unhandled event: $event") 70 | } 71 | }.exhaustive 72 | } 73 | } catch (e: IOException) { 74 | GlobalState.dispatch(GlobalState.GlobalAction.RemoveSession(session)) 75 | val reason = closeReason.await() 76 | logger.debug { "onClose ${session.userId} $reason ${e.message}" } 77 | } catch (e: ClosedReceiveChannelException) { 78 | GlobalState.dispatch(GlobalState.GlobalAction.RemoveSession(session)) 79 | val reason = closeReason.await() 80 | logger.error { "onClose ${session.userId} $reason" } 81 | } catch (e: ClosedSendChannelException) { 82 | GlobalState.dispatch(GlobalState.GlobalAction.RemoveSession(session)) 83 | val reason = closeReason.await() 84 | logger.error { "onClose ${session.userId} $reason" } 85 | } catch (e: Exception) { 86 | GlobalState.dispatch(GlobalState.GlobalAction.RemoveSession(session)) 87 | logger.error(e) { e.localizedMessage } 88 | logger.error { "exception onClose ${session.userId} ${e.message}" } 89 | } finally { 90 | 91 | } 92 | 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/Main.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import io.ktor.server.cio.CIO 4 | import io.ktor.server.engine.commandLineEnvironment 5 | import io.ktor.server.engine.embeddedServer 6 | import io.ktor.util.KtorExperimentalAPI 7 | 8 | @KtorExperimentalAPI 9 | fun main(args: Array) { 10 | embeddedServer(CIO, commandLineEnvironment(args)) 11 | .start(wait = true) 12 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/PentaApp.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import com.fasterxml.jackson.databind.SerializationFeature 4 | import com.fasterxml.jackson.module.kotlin.KotlinModule 5 | import com.soywiz.klogger.Logger 6 | import io.ktor.application.Application 7 | import io.ktor.application.install 8 | import io.ktor.features.CORS 9 | import io.ktor.features.CallLogging 10 | import io.ktor.features.ContentNegotiation 11 | import io.ktor.features.DefaultHeaders 12 | import io.ktor.features.XForwardedHeaderSupport 13 | import io.ktor.http.HttpMethod 14 | import io.ktor.jackson.jackson 15 | import io.ktor.sessions.SessionStorageMemory 16 | import io.ktor.sessions.Sessions 17 | import io.ktor.sessions.cookie 18 | import io.ktor.websocket.WebSockets 19 | import org.jetbrains.exposed.sql.Database 20 | import org.slf4j.event.Level 21 | import java.time.Duration 22 | 23 | private val logger = Logger("PentaApp") 24 | fun Application.main() { 25 | Logger.defaultLevel = Logger.Level.INFO 26 | install(DefaultHeaders) 27 | // TODO: fix call logging 28 | install(CallLogging) { 29 | // logger = penta.server.logger 30 | level = Level.INFO 31 | } 32 | 33 | val db = Database.connect( 34 | url = System.getenv("JDBC_DATABASE_URL"), 35 | driver = "org.postgresql.Driver" 36 | ) 37 | 38 | install(WebSockets) { 39 | // pingPeriod = Duration.ofMillis(1000) 40 | // timeout = Duration.ofMillis(2000) 41 | pingPeriodMillis = 1000 42 | timeoutMillis = 2000 43 | } 44 | 45 | // install(HttpsRedirect) 46 | // install(HSTS) 47 | install(CORS) { 48 | anyHost() 49 | method(HttpMethod.Get) 50 | allowNonSimpleContentTypes = true 51 | allowSameOrigin = true 52 | allowCredentials = true 53 | header("SESSION") 54 | // header("Set-Cookie") 55 | exposeHeader("SESSION") 56 | // exposeHeader("Set-Cookie") 57 | // maxAge = Duration.ofMinutes(20) 58 | maxAgeInSeconds = 20 * 60 59 | } 60 | install(XForwardedHeaderSupport) 61 | // install(EncryptionEnforcementFeature) 62 | 63 | // install(Metrics) { 64 | // val reporter = Slf4jReporter.forRegistry(registry) 65 | // .outputTo(log) 66 | // .convertRatesTo(TimeUnit.SECONDS) 67 | // .convertDurationsTo(TimeUnit.MILLISECONDS) 68 | // .build() 69 | // reporter.start(10, TimeUnit.SECONDS) 70 | // } 71 | install(ContentNegotiation) { 72 | jackson { 73 | registerModule(KotlinModule()) // Enable Kotlin support 74 | enable(SerializationFeature.INDENT_OUTPUT) 75 | // enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) 76 | } 77 | } 78 | install(Sessions) { 79 | cookie("SESSION", storage = SessionStorageMemory()) { 80 | cookie.path = "/" // Specify cookie's path '/' so it can be used in the whole site 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/ServerUserInfo.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import penta.UserInfo 4 | 5 | data class ServerUserInfo( 6 | val user: User, 7 | var figureId: String 8 | ) { 9 | /** 10 | * picks userId or displayname depending recipient 11 | * // TODO: let client track which UserInfo they belong to seperately 12 | * // TODO: send back via SessionEvent.SetUserInfo -> client 13 | */ 14 | fun getUserStringFor(session: UserSession) = if(user.userId == session.userId) user.userId else user.displayName 15 | 16 | fun toUserInfo(session: UserSession) = UserInfo(getUserStringFor(session), figureId) 17 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/SessionController.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import io.ktor.application.ApplicationCall 4 | import io.ktor.request.header 5 | import io.ktor.response.header 6 | import kotlin.random.Random 7 | 8 | object SessionController { 9 | const val KEY = "SESSION" 10 | private val sessions: MutableMap = mutableMapOf() 11 | 12 | fun get(call: ApplicationCall): UserSession? { 13 | return call.request.header(KEY)?.let { 14 | sessions[it] 15 | } 16 | } 17 | 18 | fun get(sessionId: String): UserSession? { 19 | return sessions[sessionId] 20 | } 21 | 22 | fun set(session: UserSession, call: ApplicationCall) { 23 | val sessionId = getUnusedSessionId() 24 | sessions[sessionId] = session 25 | call.response.header(KEY, sessionId) 26 | } 27 | 28 | private fun getUnusedSessionId(): String { 29 | while (true) { 30 | val sessionId: String = Random.nextInt().toString(16) 31 | if (!sessions.containsKey(sessionId)) return sessionId 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/SessionState.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import SessionEvent 4 | import com.soywiz.klogger.Logger 5 | import io.ktor.websocket.DefaultWebSocketServerSession 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.launch 8 | import org.reduxkotlin.Reducer 9 | import penta.BoardState 10 | import penta.PlayerIds 11 | import penta.network.GameEvent 12 | import penta.util.exhaustive 13 | import penta.util.handler 14 | 15 | data class SessionState( 16 | val boardState: BoardState = BoardState.create(), 17 | val observingSessions: Map = mapOf(), 18 | val playingUsers: Map = mapOf() 19 | ) { 20 | companion object { 21 | private val logger = Logger(this::class.simpleName!!) 22 | sealed class Actions { 23 | data class AddObserver( 24 | val session: UserSession, 25 | val wss: DefaultWebSocketServerSession 26 | ) : Actions() 27 | 28 | data class RemoveObserver( 29 | val session: UserSession 30 | ) : Actions() 31 | } 32 | 33 | val reducer: Reducer = { state, action -> 34 | // TODO: modify state 35 | when(action) { 36 | is org.reduxkotlin.ActionTypes.INIT -> { 37 | logger.info { "received INIT" } 38 | state 39 | } 40 | is org.reduxkotlin.ActionTypes.REPLACE -> { 41 | logger.info { "received REPLACE" } 42 | state 43 | } 44 | is BoardState.RemoveIllegalMove -> { 45 | state.copy( 46 | boardState = state.boardState.reduce(action) 47 | ) 48 | } 49 | is GameEvent -> { 50 | state.copy( 51 | boardState = state.boardState.reduce(action) 52 | ) 53 | } 54 | is AuthedSessionEvent -> { 55 | val user = action.user 56 | when(val action = action.event) { 57 | is SessionEvent.WrappedGameEvent -> { 58 | state.copy( 59 | boardState = state.boardState.reduce(action.event) 60 | ) 61 | } 62 | is SessionEvent.PlayerJoin -> { 63 | // TODO: validate origin of event 64 | logger.info { "TODO: check $user" } 65 | state.copy( 66 | playingUsers = state.playingUsers + (action.player to ServerUserInfo(user, action.user.figureId)) 67 | ) 68 | } 69 | is SessionEvent.PlayerLeave -> TODO() 70 | is SessionEvent.IllegalMove -> TODO() 71 | is SessionEvent.Undo -> TODO() 72 | } 73 | } 74 | is Actions -> { 75 | when (action) { 76 | is Actions.AddObserver -> { 77 | GlobalScope.launch(handler) { 78 | state.observingSessions.forEach { (session, wss) -> 79 | if (wss != action.wss) { 80 | // TODO: wrap Move into other object 81 | // TODO: put observer join/leave 82 | // wss.outgoing.send( 83 | // Frame.Text( 84 | // json.stringify(GameEvent.serializer(), GameEvent.ObserverLeave(session.userId)) 85 | // ) 86 | // ) 87 | } 88 | } 89 | } 90 | 91 | state.copy(observingSessions = state.observingSessions + (action.session to action.wss)) 92 | } 93 | is Actions.RemoveObserver -> { 94 | GlobalScope.launch(handler) { 95 | state.observingSessions.forEach { (session, wss) -> 96 | if (wss != state.observingSessions[action.session]) { 97 | // TODO: wrap Move into other object 98 | // TODO: put observer join/leave 99 | // wss.outgoing.send( 100 | // Frame.Text( 101 | // json.stringify(GameEvent.serializer(), GameEvent.ObserverJoin(session.userId)) 102 | // ) 103 | // ) 104 | } 105 | } 106 | } 107 | 108 | state.copy(observingSessions = state.observingSessions - action.session) 109 | } 110 | }.exhaustive 111 | } 112 | else -> { 113 | error("$action is of unhandled type") 114 | } 115 | } 116 | 117 | } 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/StatusPages.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import io.ktor.application.Application 4 | import io.ktor.application.call 5 | import io.ktor.application.install 6 | import io.ktor.features.StatusPages 7 | import io.ktor.http.HttpStatusCode 8 | import io.ktor.response.respond 9 | import java.io.PrintWriter 10 | import java.io.StringWriter 11 | 12 | fun Application.install() { 13 | install(StatusPages) { 14 | exception { cause -> 15 | cause.printStackTrace() 16 | call.respond( 17 | HttpStatusCode.InternalServerError, 18 | StackTraceMessage(cause) 19 | ) 20 | } 21 | exception { cause -> 22 | call.respond( 23 | HttpStatusCode.NotAcceptable, 24 | StackTraceMessage(cause) 25 | ) 26 | } 27 | exception { cause -> 28 | call.respond( 29 | HttpStatusCode.NotAcceptable, 30 | StackTraceMessage(cause) 31 | ) 32 | } 33 | } 34 | } 35 | 36 | val Throwable.stackTraceString: String 37 | get() { 38 | val sw = StringWriter() 39 | this.printStackTrace(PrintWriter(sw)) 40 | return sw.toString() 41 | } 42 | 43 | data class StackTraceMessage(private val e: Throwable) { 44 | val exception: String = e.javaClass.name 45 | val message: String = e.message ?: "" 46 | val stacktrace: List = e.stackTraceString.split('\n') 47 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/StoreExt.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.reduxkotlin.Dispatcher 5 | import org.reduxkotlin.GetState 6 | import org.reduxkotlin.Reducer 7 | import org.reduxkotlin.Store 8 | import org.reduxkotlin.StoreSubscriber 9 | import org.reduxkotlin.StoreSubscription 10 | import kotlin.coroutines.CoroutineContext 11 | 12 | fun sameThreadEnforcementWrapper( 13 | store: Store, 14 | context: CoroutineContext 15 | ): Store { 16 | return object : Store { 17 | override var dispatch: Dispatcher 18 | get() = { 19 | runBlocking(context) { 20 | store.dispatch(it) 21 | } 22 | } 23 | set(value) { 24 | store.dispatch = value 25 | } 26 | override val getState: GetState 27 | get() = { 28 | runBlocking(context) { 29 | store.getState() 30 | } 31 | } 32 | 33 | override val replaceReducer: (Reducer) -> Unit 34 | get() = { 35 | runBlocking(context) { 36 | store.replaceReducer(it) 37 | } 38 | } 39 | override val subscribe: (StoreSubscriber) -> StoreSubscription 40 | get() = { 41 | runBlocking(context) { 42 | store.subscribe(it) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/User.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | sealed class User { 4 | abstract val userId: String 5 | abstract val displayName: String 6 | 7 | data class RegisteredUser( 8 | override val userId: String, 9 | var displayNameField: String? = null, 10 | var passwordHash: String? = null 11 | ) : User() { 12 | override val displayName: String 13 | get() = displayNameField ?: userId 14 | } 15 | 16 | data class TemporaryUser( 17 | override val userId: String 18 | ) : User() { 19 | override val displayName: String 20 | get() = "guest_$userId" 21 | } 22 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/UserManager.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 3 | import org.jetbrains.exposed.sql.StdOutSqlLogger 4 | import org.jetbrains.exposed.sql.addLogger 5 | import penta.server.db.User as DBUser 6 | import org.jetbrains.exposed.sql.transactions.transaction 7 | import penta.server.db.Users 8 | import penta.server.db.connect 9 | import penta.server.db.findOrCreate 10 | 11 | object UserManager { 12 | fun findDBUser(userId: String) = transaction(connect()) { 13 | addLogger(StdOutSqlLogger) 14 | DBUser.find(Users.userId eq userId).firstOrNull() 15 | } 16 | 17 | fun find(userId: String): User? { 18 | return findDBUser(userId)?.let { 19 | convert(it) 20 | } 21 | } 22 | 23 | fun convert(user: DBUser): User { 24 | return user.let { 25 | if(it.temporaryUser) { 26 | User.TemporaryUser( 27 | userId = it.userId 28 | ) 29 | } else { 30 | User.RegisteredUser( 31 | userId = it.userId, 32 | displayNameField = it.displayName, 33 | passwordHash = it.passwordHash 34 | ) 35 | } 36 | 37 | } 38 | } 39 | 40 | fun toDBUser(user: User) = transaction { 41 | addLogger(StdOutSqlLogger) 42 | when(user) { 43 | is User.RegisteredUser -> { 44 | findOrCreate(user.userId) { 45 | passwordHash = user.passwordHash ?: error("null in passwordhash") 46 | userId = user.userId 47 | displayName = user.displayNameField 48 | temporaryUser = false 49 | } 50 | } 51 | is User.TemporaryUser -> { 52 | findOrCreate(user.userId) { 53 | passwordHash = null 54 | userId = user.userId 55 | displayName = null 56 | temporaryUser = true 57 | } 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/UserSession.kt: -------------------------------------------------------------------------------- 1 | package penta.server 2 | 3 | data class UserSession( 4 | val userId: String 5 | ) { 6 | fun asUser(): User { 7 | // TODO: retreive user from db 8 | return User.TemporaryUser(userId) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/db/DB.kt: -------------------------------------------------------------------------------- 1 | package penta.server.db 2 | 3 | import kotlinx.serialization.builtins.list 4 | import kotlinx.serialization.list 5 | import org.jetbrains.exposed.sql.Database 6 | import org.jetbrains.exposed.sql.SchemaUtils 7 | import org.jetbrains.exposed.sql.StdOutSqlLogger 8 | import org.jetbrains.exposed.sql.addLogger 9 | import org.jetbrains.exposed.sql.transactions.transaction 10 | import penta.logic.GameType 11 | import penta.network.GameEvent 12 | import penta.util.json 13 | import java.util.UUID 14 | 15 | fun connect() = Database.connect( 16 | url = System.getenv("JDBC_DATABASE_URL"), 17 | driver = "org.postgresql.Driver" 18 | ) 19 | 20 | fun main() { 21 | val db = Database.connect( 22 | url = System.getenv("JDBC_DATABASE_URL"), 23 | driver = "org.postgresql.Driver" 24 | ) 25 | 26 | transaction { 27 | addLogger(StdOutSqlLogger) 28 | 29 | SchemaUtils.drop( 30 | Users, PlayingUsers, Games, UserInGames, 31 | inBatch = true 32 | ) 33 | } 34 | transaction { 35 | addLogger(StdOutSqlLogger) 36 | 37 | SchemaUtils.createMissingTablesAndColumns( 38 | Users, PlayingUsers, Games, UserInGames, 39 | inBatch = false 40 | ) 41 | } 42 | 43 | val testUser = transaction { 44 | findOrCreate("TesUser") { 45 | userId = "TestUser" 46 | displayName = "Test User" 47 | passwordHash = "abcdefg" 48 | temporaryUser = false 49 | } 50 | } 51 | println("testUser: $testUser") 52 | 53 | transaction { 54 | addLogger(StdOutSqlLogger) 55 | 56 | val someuser = findOrCreate("someuser") { 57 | passwordHash = "abcdefgh" 58 | temporaryUser = true 59 | } 60 | 61 | println("someuser: $someuser") 62 | 63 | val newGame = Game.new(UUID.randomUUID()) { 64 | gameId = "game_0" 65 | history = json.stringify( 66 | GameEvent.serializer().list, listOf( 67 | // GameEvent.PlayerJoin(PlayerState("someuser", "tiangle")), 68 | GameEvent.SetGameType(GameType.TWO), 69 | GameEvent.InitGame 70 | ) 71 | ) 72 | owner = someuser 73 | // players = SizedCollection( 74 | // listOf( 75 | // someuser 76 | // ) 77 | // ) 78 | } 79 | } 80 | 81 | // TODO: figure out how to check if id exists / insertOrReplace 82 | } 83 | 84 | //object DBVersions : IntIdTable() { 85 | // val version = integer("version").default(0) 86 | //} 87 | // 88 | //class DBVersion(id: EntityID) : IntEntity(id) { 89 | // companion object : IntEntityClass(DBVersions) 90 | // 91 | // var version by DBVersions.version 92 | //} -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/db/Games.kt: -------------------------------------------------------------------------------- 1 | package penta.server.db 2 | 3 | import org.jetbrains.exposed.dao.UUIDEntity 4 | import org.jetbrains.exposed.dao.UUIDEntityClass 5 | import org.jetbrains.exposed.dao.id.EntityID 6 | import org.jetbrains.exposed.dao.id.UUIDTable 7 | import org.jetbrains.exposed.sql.Table 8 | import java.util.UUID 9 | 10 | object Games : UUIDTable() { 11 | val gameId = varchar("gameId", 50) 12 | .uniqueIndex("gameId") 13 | val history = jsonb2("history")//, json, GameEvent.serializer().list) 14 | val owner = reference("owner", Users) 15 | init { 16 | gameId.defaultValueFun = { "game_$id" } 17 | } 18 | } 19 | 20 | class Game(id: EntityID) : UUIDEntity(id) { 21 | companion object : UUIDEntityClass(Games) 22 | 23 | var gameId by Games.gameId 24 | var history by Games.history 25 | var owner by User referencedOn Games.owner 26 | var playingUsers by PlayingUser via UserInGames 27 | } 28 | 29 | object UserInGames: Table("user_in_game") { 30 | val gameId = reference("game", Games) 31 | val playerInGame = reference("playerInGame", PlayingUsers) 32 | override val primaryKey: PrimaryKey = PrimaryKey(gameId, playerInGame) 33 | } 34 | 35 | object PlayingUsers: UUIDTable("playingUsers") { 36 | val gameId = reference("game", Games)//, onDelete = ReferenceOption.CASCADE) 37 | val userId = reference("user", Users)//, onDelete = ReferenceOption.CASCADE) 38 | val player = varchar("player", 20) 39 | val shape = varchar("shape", 20) 40 | init { 41 | index(true, gameId, player) 42 | } 43 | } 44 | 45 | class PlayingUser(id: EntityID) : UUIDEntity(id) { 46 | companion object : UUIDEntityClass(PlayingUsers) 47 | var game by Game referencedOn PlayingUsers.gameId 48 | var user by User referencedOn PlayingUsers.userId 49 | var player by PlayingUsers.player 50 | var shape by PlayingUsers.shape 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/db/Users.kt: -------------------------------------------------------------------------------- 1 | package penta.server.db 2 | 3 | import org.jetbrains.exposed.dao.UUIDEntity 4 | import org.jetbrains.exposed.dao.UUIDEntityClass 5 | import org.jetbrains.exposed.dao.id.EntityID 6 | import org.jetbrains.exposed.dao.id.UUIDTable 7 | import org.jetbrains.exposed.sql.Transaction 8 | import org.jetbrains.exposed.sql.transactions.transaction 9 | import java.util.UUID 10 | 11 | object Users: UUIDTable() { 12 | val userId = varchar("userId", 50) 13 | .uniqueIndex() 14 | val passwordHash = varchar("passwordHash", 50) 15 | .nullable() 16 | val displayName = varchar("displayName", 50) 17 | .nullable() 18 | val temporaryUser = bool("temporaryUser") 19 | 20 | init { 21 | // displayName.defaultValueFun = { null } 22 | } 23 | } 24 | 25 | class User(id: EntityID) : UUIDEntity(id) { 26 | companion object : UUIDEntityClass(Users) 27 | 28 | var userId by Users.userId 29 | var passwordHash by Users.passwordHash 30 | var displayName by Users.displayName 31 | var players by Game via PlayingUsers 32 | var temporaryUser by Users.temporaryUser 33 | 34 | override fun toString(): String { 35 | return transaction { 36 | "User(id=$id, userId=$userId, passwordHash=$passwordHash, displayName=$displayName, players=${players.map { it }})" 37 | } 38 | } 39 | } 40 | 41 | fun Transaction.findOrCreate( 42 | userId: String, 43 | builder: User.() -> Unit 44 | ): User { 45 | return User.find { 46 | Users.userId eq userId 47 | }.firstOrNull() 48 | ?: let { 49 | println("creating user") 50 | User.new { 51 | this.userId = userId 52 | this.builder() 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/db/jsonb.kt: -------------------------------------------------------------------------------- 1 | package penta.server.db 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import kotlinx.serialization.DeserializationStrategy 5 | import kotlinx.serialization.KSerializer 6 | import org.jetbrains.exposed.sql.Column 7 | import org.jetbrains.exposed.sql.ColumnType 8 | import org.jetbrains.exposed.sql.StringColumnType 9 | import org.jetbrains.exposed.sql.Table 10 | import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi 11 | import org.postgresql.util.PGobject 12 | import java.sql.PreparedStatement 13 | 14 | /** 15 | * Created by quangio. 16 | */ 17 | 18 | fun Table.jsonb(name: String, json: kotlinx.serialization.json.Json, serializer: KSerializer): Column 19 | = registerColumn(name, Json(json, serializer)) 20 | fun Table.jsonb2(name: String): Column 21 | = registerColumn(name, JsonString()) 22 | 23 | 24 | private class JsonString(): ColumnType() { 25 | override fun sqlType() = "jsonb" 26 | override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { 27 | val obj = PGobject() 28 | obj.type = "jsonb" 29 | obj.value = value as String 30 | stmt.set(index, obj) 31 | } 32 | 33 | override fun valueToString(value: Any?): String { 34 | if(value is PGobject) { 35 | return value.value 36 | } 37 | return value as String 38 | } 39 | override fun valueFromDB(value: Any): Any { 40 | if (value !is PGobject) { 41 | // We didn't receive a PGobject (the format of stuff actually coming from the DB). 42 | // In that case "value" should already be an object of type T. 43 | return value 44 | } 45 | 46 | return value.value 47 | } 48 | override fun nonNullValueToString(value: Any): String { 49 | if(value is PGobject) { 50 | return value.value 51 | } 52 | return value as String 53 | } 54 | } 55 | 56 | 57 | private class Json(private val json: kotlinx.serialization.json.Json, private val serializer: KSerializer) : ColumnType() { 58 | override fun sqlType() = "jsonb" 59 | 60 | override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) { 61 | val obj = PGobject() 62 | obj.type = "jsonb" 63 | obj.value = value as String 64 | stmt.set(index, obj) 65 | } 66 | 67 | override fun valueFromDB(value: Any): Any { 68 | if (value !is PGobject) { 69 | // We didn't receive a PGobject (the format of stuff actually coming from the DB). 70 | // In that case "value" should already be an object of type T. 71 | return value 72 | } 73 | 74 | // We received a PGobject, deserialize its String value. 75 | return try { 76 | json.parse(serializer, value.value) 77 | } catch (e: Exception) { 78 | e.printStackTrace() 79 | throw RuntimeException("Can't parse JSON: $value") 80 | } 81 | } 82 | 83 | override fun notNullValueToDB(value: Any): Any = json.stringify(serializer, value as T) 84 | override fun nonNullValueToString(value: Any): String { 85 | println("serializing: $value") 86 | return "'${json.stringify(serializer, value as T)}'" 87 | } 88 | } -------------------------------------------------------------------------------- /backend/src/main/kotlin/penta/server/feature/EncryptionEnforcementFeature.kt: -------------------------------------------------------------------------------- 1 | package penta.server.feature 2 | 3 | import io.ktor.application.ApplicationCallPipeline 4 | import io.ktor.application.ApplicationFeature 5 | import io.ktor.application.call 6 | import io.ktor.features.origin 7 | import io.ktor.util.AttributeKey 8 | 9 | internal object EncryptionEnforcementFeature : ApplicationFeature { 10 | 11 | override val key = AttributeKey("Baku: encryption enforcement feature") 12 | 13 | override fun install(pipeline: ApplicationCallPipeline, configure: Unit.() -> Unit) { 14 | Unit.configure() 15 | 16 | pipeline.intercept(ApplicationCallPipeline.Features) { 17 | if (call.request.origin.scheme != "https") 18 | throw IllegalStateException( 19 | "This API must only be used over an encrypted connection." 20 | ) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /backend/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | watch = [ penta.server ] 6 | } 7 | 8 | application { 9 | modules = [ 10 | penta.server.PentaAppKt.main, 11 | penta.server.StatusPagesKt.install, 12 | penta.server.RoutesKt.routes 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V1.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE games ( 3 | id uuid NOT NULL, 4 | "gameId" character varying(50) NOT NULL, 5 | history jsonb NOT NULL, 6 | owner uuid NOT NULL 7 | ); 8 | 9 | 10 | CREATE TABLE players_in_games ( 11 | "user" uuid NOT NULL, 12 | game uuid NOT NULL 13 | ); 14 | 15 | 16 | CREATE TABLE users ( 17 | id uuid NOT NULL, 18 | "userId" character varying(50) NOT NULL, 19 | "passwordHash" character varying(50), 20 | "displayName" character varying(50), 21 | "temporaryUser" boolean NOT NULL 22 | ); 23 | 24 | 25 | ALTER TABLE games 26 | ADD CONSTRAINT games_pkey PRIMARY KEY (id); 27 | 28 | ALTER TABLE players_in_games 29 | ADD CONSTRAINT pk_players_in_games PRIMARY KEY ("user", game); 30 | 31 | ALTER TABLE users 32 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 33 | 34 | ALTER TABLE games 35 | ADD CONSTRAINT fk_games_owner_id FOREIGN KEY (owner) REFERENCES public.users(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 36 | 37 | ALTER TABLE players_in_games 38 | ADD CONSTRAINT fk_players_in_games_game_id FOREIGN KEY (game) REFERENCES public.games(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 39 | 40 | ALTER TABLE players_in_games 41 | ADD CONSTRAINT fk_players_in_games_user_id FOREIGN KEY ("user") REFERENCES public.users(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 42 | 43 | ALTER TABLE users 44 | ADD CONSTRAINT users_userid_unique UNIQUE ("userId"); 45 | -------------------------------------------------------------------------------- /backend/src/main/resources/db/migration/V2.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE players_in_games; 3 | 4 | CREATE TABLE playingusers ( 5 | id uuid NOT NULL, 6 | game uuid NOT NULL, 7 | "user" uuid NOT NULL, 8 | player character varying(20) NOT NULL, 9 | shape character varying(20) NOT NULL 10 | ); 11 | 12 | 13 | CREATE TABLE user_in_game ( 14 | game uuid NOT NULL, 15 | "playerInGame" uuid NOT NULL 16 | ); 17 | 18 | ALTER TABLE playingusers 19 | ADD CONSTRAINT playingusers_pkey PRIMARY KEY (id); 20 | 21 | ALTER TABLE user_in_game 22 | ADD CONSTRAINT pk_user_in_game PRIMARY KEY (game, "playerInGame"); 23 | 24 | ALTER TABLE playingusers 25 | ADD CONSTRAINT playingusers_game_player_unique UNIQUE (game, player); 26 | 27 | ALTER TABLE playingusers 28 | ADD CONSTRAINT fk_playingusers_game_id FOREIGN KEY (game) REFERENCES public.games(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 29 | 30 | ALTER TABLE playingusers 31 | ADD CONSTRAINT fk_playingusers_user_id FOREIGN KEY ("user") REFERENCES public.users(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 32 | 33 | ALTER TABLE user_in_game 34 | ADD CONSTRAINT fk_user_in_game_game_id FOREIGN KEY (game) REFERENCES public.games(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 35 | 36 | ALTER TABLE user_in_game 37 | ADD CONSTRAINT fk_user_in_game_playeringame_id FOREIGN KEY ("playerInGame") REFERENCES public.playingusers(id) ON UPDATE RESTRICT ON DELETE RESTRICT; 38 | -------------------------------------------------------------------------------- /backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %-55(%d{HH:mm:ss.SSS} [%thread] %-20(.\(%F:%L\))) %-5level - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/src/test/kotlin/tests/DBTests.kt: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import com.soywiz.klogger.Logger 4 | import org.jetbrains.exposed.sql.Database 5 | import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger 6 | import org.jetbrains.exposed.sql.addLogger 7 | import org.jetbrains.exposed.sql.transactions.transaction 8 | import penta.server.db.User 9 | import kotlin.test.BeforeTest 10 | import kotlin.test.Test 11 | 12 | val logger = Logger("DBTests") 13 | 14 | @BeforeTest 15 | fun connect() { 16 | Database.connect( 17 | url = System.getenv("DEV_DATABASE_URL"), 18 | driver = "org.postgresql.Driver" 19 | ) 20 | } 21 | 22 | @Test 23 | fun `users select all`() { 24 | 25 | val users = transaction { 26 | addLogger(Slf4jSqlDebugLogger) 27 | User.all() 28 | } 29 | 30 | logger.info { "users: $users" } 31 | } -------------------------------------------------------------------------------- /backend/src/test/kotlin/tests/Tests.kt: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import com.soywiz.klogger.Logger 4 | import org.junit.Test 5 | import org.reduxkotlin.applyMiddleware 6 | import org.reduxkotlin.createStore 7 | import penta.BoardState 8 | 9 | class Tests { 10 | private val logger = Logger(this::class.simpleName!!) 11 | @Test 12 | fun test() { 13 | val boardStore: org.reduxkotlin.Store = createStore( 14 | BoardState.Companion::reduceFunc, 15 | BoardState.create(), 16 | applyMiddleware(/*loggingMiddleware(logger)*/) 17 | ) 18 | logger.info { "initialized" } 19 | // boardStore.dispatch(Action(PentaMove.PlayerJoin(PlayerState("eve", "square")))) 20 | // boardStore.dispatch(Action(PentaMove.InitGame)) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /backend/src/test/kotlin/util/ResetDB.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import org.jetbrains.exposed.sql.Database 4 | import org.jetbrains.exposed.sql.SchemaUtils 5 | import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger 6 | import org.jetbrains.exposed.sql.addLogger 7 | import org.jetbrains.exposed.sql.transactions.transaction 8 | import penta.server.db.Games 9 | import penta.server.db.PlayingUsers 10 | import penta.server.db.UserInGames 11 | import penta.server.db.Users 12 | 13 | object ResetDB { 14 | @JvmStatic 15 | fun main(args: Array) { 16 | val db = Database.connect( 17 | url = System.getenv("JDBC_DATABASE_URL"), 18 | driver = "org.postgresql.Driver" 19 | ) 20 | 21 | transaction { 22 | addLogger(Slf4jSqlDebugLogger) 23 | 24 | SchemaUtils.drop( 25 | Users, 26 | PlayingUsers, 27 | Games, 28 | UserInGames, 29 | inBatch = false 30 | ) 31 | } 32 | 33 | transaction { 34 | addLogger(Slf4jSqlDebugLogger) 35 | 36 | SchemaUtils.createMissingTablesAndColumns( 37 | Users, 38 | PlayingUsers, 39 | Games, 40 | UserInGames, 41 | inBatch = false 42 | ) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") version Kotlin.version 3 | id("kotlinx-serialization") version Kotlin.version 4 | id("de.fayard.refreshVersions") // version "0.8.6" 5 | `maven-publish` 6 | } 7 | 8 | allprojects { 9 | group = "moe.nikky.penta" 10 | 11 | // TODO: remove 12 | repositories { 13 | maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") 14 | mavenCentral() 15 | jcenter() 16 | maven(url = "https://jcenter.bintray.com/") 17 | maven(url = "https://dl.bintray.com/kotlin/kotlinx") 18 | maven(url = "https://dl.bintray.com/kotlin/kotlin-js-wrappers") 19 | maven(url = "https://dl.bintray.com/kotlin/ktor") 20 | maven(url = "https://dl.bintray.com/data2viz/data2viz/") 21 | maven(url = "https://dl.bintray.com/korlibs/korlibs/") 22 | maven(url = "https://dl.bintray.com/kotlin/exposed") 23 | maven(url = "https://dl.bintray.com/nwillc/maven") 24 | // mavenLocal() 25 | } 26 | 27 | val privateScript = rootDir.resolve("private.gradle.kts") 28 | if(privateScript.exists()) { 29 | apply(from = privateScript) 30 | } else { 31 | val DEV_JDBC_DATABASE_URL by extra("jdbc:postgresql://localhost:5432/pentagame?user=postgres") 32 | } 33 | } 34 | 35 | // heroku stage 36 | val stage = tasks.create("stage") { 37 | dependsOn("clean") 38 | dependsOn(":backend:flywayMigrate") 39 | dependsOn(":backend:flywayValidate") 40 | dependsOn(":backend:shadowJar") 41 | 42 | doLast { 43 | logger.lifecycle("jar was compiled") 44 | } 45 | } 46 | 47 | // debugging 48 | System.getenv().forEach { (key, value) -> 49 | logger.info("$key : $value") 50 | } 51 | 52 | tasks.register("hello") { 53 | description = "Hello World" 54 | group = "help" 55 | doLast { 56 | logger.lifecycle("Hello World") 57 | } 58 | } 59 | 60 | kotlin { 61 | js { 62 | useCommonJs() 63 | browser { 64 | runTask { 65 | sourceMaps = true 66 | } 67 | webpackTask { 68 | sourceMaps = true 69 | } 70 | } 71 | compilations.all { 72 | kotlinOptions { 73 | sourceMap = true 74 | metaInfo = true 75 | main = "call" 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | idea 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | jcenter() 9 | maven(url = "https://dl.bintray.com/kotlin/kotlinx.html/") { 10 | name = "kotlinx bintray" 11 | } 12 | } 13 | 14 | dependencies { 15 | api(group = "com.squareup", name = "kotlinpoet", version = "1.4.1") 16 | api(group = "com.marvinformatics.apgdiff", name = "apgdiff", version = "2.5.0.20160618") 17 | api("io.ktor:ktor-client-cio:1.3.0") 18 | } 19 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | //pluginManagement { 2 | // repositories { 3 | // maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") 4 | // gradlePluginPortal() 5 | // } 6 | //} 7 | //rootProject.name = "buildSrc" 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/ConstantsGenerator.kt: -------------------------------------------------------------------------------- 1 | import com.squareup.kotlinpoet.ClassName 2 | import com.squareup.kotlinpoet.FileSpec 3 | import com.squareup.kotlinpoet.KModifier 4 | import com.squareup.kotlinpoet.PropertySpec 5 | import com.squareup.kotlinpoet.TypeSpec 6 | import java.io.File 7 | import java.io.Serializable 8 | 9 | fun generateConstants(folder: File, pkg: String = "", className: String, configure: ConstantsConfiguration.() -> Unit) { 10 | val config = ConstantsConfiguration(pkg, className) 11 | config.configure() 12 | 13 | val constantBuilder = 14 | TypeSpec.objectBuilder(ClassName(config.pkg, config.className)) 15 | 16 | config.fields.forEach { (key, value) -> 17 | when (value) { 18 | is String -> { 19 | constantBuilder.addProperty( 20 | PropertySpec.builder( 21 | key, 22 | String::class, 23 | KModifier.CONST 24 | ) 25 | .initializer("%S", value) 26 | .build() 27 | ) 28 | } 29 | is Int -> { 30 | constantBuilder.addProperty( 31 | PropertySpec.builder( 32 | key, 33 | Int::class, 34 | KModifier.CONST 35 | ) 36 | .initializer("%L", value) 37 | .build() 38 | ) 39 | } 40 | } 41 | } 42 | 43 | val source = FileSpec.get(config.pkg, constantBuilder.build()) 44 | source.writeTo(folder) 45 | } 46 | 47 | class ConstantsConfiguration(val pkg: String, val className: String) : Serializable { 48 | var fields: Map = mapOf() 49 | private set 50 | 51 | fun field(name: String) = ConstantField(name) 52 | 53 | infix fun ConstantField.value(value: String) { 54 | fields += this.name to value 55 | } 56 | 57 | infix fun ConstantField.value(value: Int) { 58 | fields += this.name to value 59 | } 60 | } 61 | 62 | data class ConstantField ( 63 | val name: String 64 | ) 65 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Versions.kt: -------------------------------------------------------------------------------- 1 | 2 | object Kotlin { 3 | // val version = "_" 4 | // val version = "1.3.61" 5 | // val version = "1.3.70-eap-184" 6 | val version = "1.3.70" 7 | } 8 | 9 | object Data2Viz { 10 | // const val version = "_" 11 | const val version = "0.8.0-RC10" 12 | const val group = "io.data2viz.d2v" 13 | } 14 | 15 | object Ktor { 16 | // const val version = "1.2.6" 17 | const val version = "1.3.2" 18 | // const val version = "1.3.0-rc3-1.3.70-eap-42" 19 | } 20 | 21 | object Logback { 22 | const val version = "1.2.3" 23 | } 24 | 25 | object Serialization { 26 | // const val version = "0.14.0" 27 | // const val version = "0.14.0-1.3.70-eap-134" 28 | const val version = "0.20.0-1.3.70-eap-274" 29 | } 30 | 31 | object Coroutines { 32 | // const val version = "1.3.3" 33 | const val version = "1.3.3-1.3.70-eap-42" 34 | } 35 | 36 | object KotlinLogging { 37 | const val version = "1.7.8" 38 | } 39 | 40 | object KLogger { 41 | const val version = "1.8.1" 42 | } 43 | 44 | object ReduxKotlin { 45 | const val version = "0.2.9" 46 | } 47 | 48 | object KSvg { 49 | const val version = "3.0.0-SNAPSHOT" 50 | } 51 | 52 | object Exposed { 53 | const val version = "0.20.3" 54 | } 55 | 56 | object Postgres { 57 | const val version = "42.2.2" 58 | } 59 | 60 | object Flyway { 61 | const val version = "6.2.1" 62 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/client.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.client.engine.cio.CIO 2 | import io.ktor.client.HttpClient 3 | import io.ktor.client.request.request 4 | import io.ktor.client.request.url 5 | import io.ktor.client.statement.HttpResponse 6 | import io.ktor.http.HttpMethod 7 | import io.ktor.http.isSuccess 8 | import io.ktor.util.cio.writeChannel 9 | import io.ktor.utils.io.copyAndClose 10 | import kotlinx.coroutines.runBlocking 11 | import java.io.File 12 | import java.io.IOException 13 | import java.net.URL 14 | 15 | val client = HttpClient(CIO) { 16 | 17 | } 18 | data class HttpClientException(val response: HttpResponse) : IOException("HTTP Error ${response.status}") 19 | 20 | fun downloadFile(url: String, file: File) { 21 | return runBlocking { 22 | val response = client.request { 23 | url(URL(url)) 24 | method = HttpMethod.Get 25 | } 26 | if (!response.status.isSuccess()) { 27 | throw HttpClientException(response) 28 | } 29 | response.content.copyAndClose(file.writeChannel()) 30 | file 31 | } 32 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/org/gradle/kotlin/dsl/Extensions.kt: -------------------------------------------------------------------------------- 1 | package org.gradle.kotlin.dsl 2 | 3 | import org.gradle.api.Project 4 | import java.io.ByteArrayOutputStream 5 | import java.net.URI 6 | 7 | fun ktor(module: String? = null, version: String? = null): Any = 8 | "io.ktor:${module?.let { "ktor-$module" } ?: "ktor"}:${version ?: Ktor.version}" 9 | 10 | fun d2v(module: String, version: String? = Data2Viz.version): String = 11 | "${Data2Viz.group}:${module}" + (version?.let { ":$it" } ?: "") 12 | 13 | fun Project.captureExec(vararg args: Any): String { 14 | return ByteArrayOutputStream().use { os -> 15 | val result = exec { 16 | commandLine(*args) 17 | standardOutput = os 18 | } 19 | os.toString() 20 | } 21 | } 22 | 23 | data class JDBC( 24 | val host: String, 25 | val database: String, 26 | val user: String?, 27 | val password: String? 28 | ) 29 | 30 | fun Project.split_jdbc(connectionString: String): JDBC { 31 | val cleanURI = connectionString.substring(5) 32 | 33 | val uri = URI.create(cleanURI) 34 | // logger.info(uri.scheme) 35 | // logger.info(uri.host) 36 | // logger.info(uri.userInfo) 37 | // logger.info(uri.query) 38 | // logger.info("" + uri.port) 39 | // logger.info(uri.path) 40 | 41 | val query = uri.query.split('&').map { 42 | val (a, b) = it.split('=') 43 | a to b 44 | }.toMap() 45 | 46 | return JDBC( 47 | host = uri.host, 48 | database = uri.path.substringAfterLast('/'), 49 | user = query["user"], 50 | password = query["password"] 51 | ) 52 | 53 | } 54 | 55 | fun Project.pg_dump(connectionString: String, target: String, extraArgs: Array) { 56 | val cleanURI = connectionString.substring(5) 57 | 58 | val uri = URI.create(cleanURI) 59 | // logger.info(uri.scheme) 60 | // logger.info(uri.host) 61 | // logger.info(uri.userInfo) 62 | // logger.info(uri.query) 63 | // logger.info("" + uri.port) 64 | // logger.info(uri.path) 65 | 66 | val query = uri.query.split('&').map { 67 | val (a, b) = it.split('=') 68 | a to b 69 | }.toMap() 70 | 71 | pg_dump(uri.host, uri.path.substringAfterLast('/'), target, query["user"], query["password"], extraArgs) 72 | } 73 | 74 | fun Project.pg_dump(host: String, database: String, target: String, user: String? = null, password: String? = null, extraArgs: Array = arrayOf()) { 75 | 76 | exec { 77 | password?.let { 78 | environment("PGPASSWORD", it) 79 | } 80 | commandLine( 81 | "pg_dump", *extraArgs, 82 | "-h", host, 83 | "-U", user ?: "", 84 | "-d", database, 85 | "-f", target 86 | ) 87 | logger.lifecycle("executing: "+ commandLine.joinToString(" ")) 88 | } 89 | } -------------------------------------------------------------------------------- /frontend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.undercouch.gradle.tasks.download.org.apache.commons.codec.digest.DigestUtils 2 | plugins { 3 | kotlin("js") 4 | id("de.fayard.dependencies") 5 | } 6 | 7 | kotlin { 8 | target { 9 | // new kotlin("js") stuff 10 | useCommonJs() 11 | browser { 12 | dceTask { 13 | dceOptions { 14 | keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io") 15 | } 16 | } 17 | runTask { 18 | sourceMaps = true 19 | } 20 | webpackTask { 21 | sourceMaps = true 22 | } 23 | } 24 | compilations.all { 25 | kotlinOptions { 26 | sourceMap = true 27 | sourceMapPrefix = "" 28 | metaInfo = true 29 | // moduleKind = "amd" 30 | // sourceMapEmbedSources = "always" 31 | } 32 | } 33 | } 34 | 35 | 36 | sourceSets { 37 | val main by getting { 38 | dependencies { 39 | 40 | } 41 | } 42 | 43 | val test by getting { 44 | dependencies { 45 | implementation(kotlin("stdlib")) 46 | } 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation(kotlin("stdlib")) 53 | 54 | implementation(project(":shared")) 55 | 56 | // temp fix ? 57 | // implementation(npm("text-encoding")) 58 | } 59 | 60 | //val JsJar = tasks.getByName("JsJar") 61 | // 62 | //val unzipJsJar = tasks.create("unzipJsJar") { 63 | // dependsOn(JsJar) 64 | // group = "build" 65 | // from(zipTree(JsJar.archiveFile)) 66 | // into(JsJar.destinationDirectory.file(JsJar.archiveBaseName)) 67 | //} 68 | 69 | task("depsize") { 70 | group = "help" 71 | description = "prints dependency sizes" 72 | doLast { 73 | val formatStr = "%,10.2f" 74 | val configuration = kotlin.target.compilations.getByName("main").compileDependencyFiles as Configuration 75 | val size = configuration.resolve() 76 | .map { it.length() / (1024.0 * 1024.0) }.sum() 77 | 78 | val out = buildString { 79 | append("Total dependencies size:".padEnd(55)) 80 | append("${String.format(formatStr, size)} Mb\n\n") 81 | configuration 82 | .resolve() 83 | .sortedWith(compareBy { -it.length() }) 84 | .forEach { 85 | append(it.name.padEnd(55)) 86 | append("${String.format(formatStr, (it.length() / 1024.0))} kb\n") 87 | } 88 | } 89 | println(out) 90 | } 91 | } 92 | 93 | tasks.getByName("processResources") { 94 | val downloadCss = mutableMapOf() 95 | val processCss = mutableMapOf() 96 | 97 | filesMatching("*.html") { 98 | filter { content -> 99 | val regex = Regex("") 100 | content.replace(regex) { matchResult -> 101 | val url = matchResult.groupValues[1] 102 | val filename = url.substringAfterLast('/') 103 | val hashed = "css/" + DigestUtils.md5Hex(url) + ".css" 104 | downloadCss += hashed to url 105 | " " 106 | } 107 | } 108 | } 109 | 110 | doLast { 111 | val dir = buildDir.resolve("processedResources") 112 | .resolve("Js") 113 | .resolve("main") 114 | downloadCss.forEach { filename, url -> 115 | logger.lifecycle("downloading $url -> $filename") 116 | val destFile = dir.resolve(filename) 117 | destFile.parentFile.mkdirs() 118 | destFile.createNewFile() 119 | downloadFile(url, destFile) 120 | 121 | logger.lifecycle("filtering $filename") 122 | val ttfUrlRegex = Regex("""url\((http.*?\.ttf)\)""") 123 | destFile.writeText( 124 | destFile.readText().replace(ttfUrlRegex) { result -> 125 | val ttfUrl = result.groupValues[1] 126 | // val filename = ttfUrl.substringAfterLast('/') 127 | val newTtfPath = "ttf/" + DigestUtils.md5Hex(ttfUrl) + ".ttf" 128 | val ttfFile = dir.resolve(newTtfPath) 129 | logger.lifecycle("downloading $ttfUrl -> $newTtfPath") 130 | ttfFile.parentFile.mkdirs() 131 | ttfFile.createNewFile() 132 | downloadFile(ttfUrl, ttfFile) 133 | 134 | logger.lifecycle("replacing: $ttfUrl -> $newTtfPath") 135 | "url(../$newTtfPath) /* $url */ " 136 | } 137 | ) 138 | 139 | } 140 | } 141 | } 142 | 143 | /*** 144 | //TODO: error Task with name 'browserProductionWebpack' not found in project ':frontend'. 145 | 146 | tasks.getByName("browserProductionWebpack").apply { 147 | doLast { 148 | val rootDirPath = rootDir.absolutePath.replace('\\', '/') 149 | val mapFile = project.buildDir.resolve("distributions/${project.name}.js.map") 150 | mapFile.writeText( 151 | mapFile.readText() 152 | .replace("$rootDirPath/build/js/src/main/", "") 153 | .replace("$rootDirPath/build/src/main/", "") 154 | .replace("$rootDirPath/build/js/node_modules", "node_modules") 155 | .replace(rootDirPath, rootDir.name) 156 | ) 157 | } 158 | } 159 | **/ 160 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/Index.kt: -------------------------------------------------------------------------------- 1 | import com.ccfraser.muirwik.components.styles.mStylesProvider 2 | import com.soywiz.klogger.Logger 3 | import components.app 4 | import externals.ReduxLoggerOptionsImpl 5 | import externals.createLogger 6 | import react.dom.render 7 | import react.redux.provider 8 | import reducers.State 9 | import redux.RAction 10 | import redux.Store 11 | import redux.applyMiddleware 12 | import redux.compose 13 | import redux.createStore 14 | import redux.rEnhancer 15 | import kotlin.browser.document 16 | 17 | val initialState = State() 18 | val store: Store = createStore( 19 | State.reducer, 20 | initialState, 21 | compose( 22 | rEnhancer(), 23 | applyMiddleware( 24 | createLogger( 25 | ReduxLoggerOptionsImpl( 26 | "verbose" 27 | ) 28 | ) 29 | ), 30 | // applyMiddleware( 31 | // createLogger(object: ReduxLoggerOptions { 32 | // override var logger: Any? 33 | // get() = super.logger 34 | // set(value) {} 35 | // }) 36 | // { middleWareApi -> 37 | // { actionFun -> 38 | //// console.log("actionFun: $actionFun") 39 | // { action -> 40 | // console.log("action: $action") 41 | // actionFun(action) 42 | // } 43 | // } 44 | // } 45 | // ), 46 | js("if(window.__REDUX_DEVTOOLS_EXTENSION__ )window.__REDUX_DEVTOOLS_EXTENSION__ ();else(function(f){return f;});") 47 | ) 48 | ) 49 | 50 | fun main() { 51 | // logger.info { "store initialized" } 52 | // store.dispatch( 53 | // Action( 54 | // PentaMove.PlayerJoin(PlayerState("eve", "square")) 55 | // ) 56 | // ) 57 | // store.dispatch( 58 | // Action( 59 | // PentaMove.InitGame 60 | // ) 61 | // ) 62 | Logger.defaultLevel = Logger.Level.DEBUG 63 | 64 | val rootDiv = document.getElementById("container") 65 | render(rootDiv) { 66 | mStylesProvider("jss-insertion-point") { 67 | provider(store) { 68 | app() 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/components/App.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import com.ccfraser.muirwik.components.HRefOptions 4 | import com.ccfraser.muirwik.components.MGridSize 5 | import com.ccfraser.muirwik.components.MGridWrap 6 | import com.ccfraser.muirwik.components.MTabIndicatorColor 7 | import com.ccfraser.muirwik.components.MTabOrientation 8 | import com.ccfraser.muirwik.components.MTabTextColor 9 | import com.ccfraser.muirwik.components.MTabVariant 10 | import com.ccfraser.muirwik.components.MTypographyVariant 11 | import com.ccfraser.muirwik.components.mGridContainer 12 | import com.ccfraser.muirwik.components.mGridItem 13 | import com.ccfraser.muirwik.components.mIcon 14 | import com.ccfraser.muirwik.components.mLink 15 | import com.ccfraser.muirwik.components.mTab 16 | import com.ccfraser.muirwik.components.mTabs 17 | import com.ccfraser.muirwik.components.mTypography 18 | import com.ccfraser.muirwik.components.spacingUnits 19 | import com.ccfraser.muirwik.components.variant 20 | import kotlinx.css.Position 21 | import kotlinx.css.backgroundColor 22 | import kotlinx.css.height 23 | import kotlinx.css.left 24 | import kotlinx.css.marginTop 25 | import kotlinx.css.padding 26 | import kotlinx.css.position 27 | import kotlinx.css.px 28 | import kotlinx.css.top 29 | import react.RBuilder 30 | import react.RComponent 31 | import react.RProps 32 | import react.RState 33 | import react.dom.br 34 | import react.dom.div 35 | import react.setState 36 | import styled.css 37 | import styled.styledDiv 38 | 39 | enum class Tabs { 40 | help, multiplayer, about, debug_game 41 | } 42 | 43 | class App : RComponent() { 44 | var tabValue: Any = Tabs.help 45 | 46 | override fun RBuilder.render() { 47 | div(classes = "absolute") { 48 | mTypography("Pentagame", variant = MTypographyVariant.h2) 49 | gameSetupControls {} 50 | mGridContainer(wrap = MGridWrap.noWrap) { 51 | css { 52 | marginTop = 3.spacingUnits 53 | // flexGrow = 1.0 54 | /*backgroundColor = Color(theme.palette.background.paper)*/ 55 | } 56 | 57 | mGridItem(xs = MGridSize.cellsAuto) { 58 | mTabs( 59 | tabValue, 60 | variant = MTabVariant.scrollable, 61 | textColor = MTabTextColor.primary, 62 | indicatorColor = MTabIndicatorColor.primary, 63 | orientation = MTabOrientation.vertical, 64 | onChange = { _, value -> 65 | setState { 66 | tabValue = value 67 | } 68 | } 69 | ) { 70 | // TODO: add conditional tabs (game list) 71 | mTab("Rules", Tabs.help, icon = mIcon("help", addAsChild = false)) 72 | mTab("Multiplayer", Tabs.multiplayer, icon = mIcon("people", addAsChild = false)) 73 | mTab("About", Tabs.about, icon = mIcon("info", addAsChild = false)) 74 | // mTab("Item Five", 4, icon = mIcon("shopping_basket", addAsChild = false)) 75 | // mTab("Item Six", 5, icon = mIcon("thumb_down", addAsChild = false)) 76 | // mTab("Item Seven", 6, icon = mIcon("thumb_up", addAsChild = false)) 77 | mTab("Debug Game", Tabs.debug_game, icon = mIcon("developer_mode", addAsChild = false)) 78 | } 79 | } 80 | 81 | mGridItem(xs = MGridSize.cellsTrue) { 82 | styledDiv { 83 | css { 84 | padding = "0.5em" 85 | } 86 | 87 | when (tabValue as Tabs) { 88 | Tabs.help -> { 89 | mTypography("Rules", paragraph = true) 90 | mTypography(text = null, paragraph = true) { 91 | mLink( 92 | text = "Illustated Rules (English)", 93 | hRefOptions = HRefOptions( 94 | href = "https://pentagame.org/pdf/Illustrated_Rules.pdf", 95 | targetBlank = true 96 | ) 97 | ) { 98 | attrs.variant = MTypographyVariant.button 99 | } 100 | } 101 | 102 | mTypography(text = null, paragraph = true) { 103 | mLink( 104 | text = "Illustated Rules (German)", 105 | hRefOptions = HRefOptions( 106 | href = "https://pentagame.org/pdf/Illustrated_Rules__German_.pdf", 107 | targetBlank = true 108 | ) 109 | ) { 110 | attrs.variant = MTypographyVariant.button 111 | } 112 | } 113 | } 114 | Tabs.multiplayer -> { 115 | textConnection {} 116 | } 117 | Tabs.about -> { 118 | mTypography("About") 119 | mLink( 120 | text = "About Pentagame", 121 | hRefOptions = HRefOptions( 122 | href = "https://pentagame.org/", 123 | targetBlank = true 124 | ) 125 | ) { 126 | attrs.variant = MTypographyVariant.button 127 | } 128 | br {} 129 | mLink( 130 | text = "Github", 131 | hRefOptions = HRefOptions( 132 | href = "https://github.com/NikkyAI/pentagame", 133 | targetBlank = true 134 | ) 135 | ) { 136 | attrs.variant = MTypographyVariant.button 137 | } 138 | } 139 | Tabs.debug_game -> { 140 | textBoardState {} 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | div(classes = "fixed-background") {} 148 | 149 | div(classes = "fixed") { 150 | pentaSvgInteractive {} 151 | } 152 | } 153 | } 154 | 155 | fun RBuilder.app() = child(App::class) {} 156 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/components/PentaSvg.kt: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import PentaBoard 4 | import actions.Action 5 | import com.github.nwillc.ksvg.RenderMode 6 | import com.github.nwillc.ksvg.elements.SVG 7 | import debug 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | import org.w3c.dom.Element 11 | import org.w3c.dom.events.Event 12 | import org.w3c.dom.events.MouseEvent 13 | import org.w3c.dom.svg.SVGCircleElement 14 | import org.w3c.dom.svg.SVGElement 15 | import penta.BoardState 16 | import penta.ConnectionState 17 | import penta.PentaMove 18 | import penta.PentagameClick 19 | import penta.PlayerIds 20 | import penta.UserInfo 21 | import react.RBuilder 22 | import react.RClass 23 | import react.RComponent 24 | import react.RProps 25 | import react.RState 26 | import react.createRef 27 | import react.invoke 28 | import react.redux.rConnect 29 | import reducers.State 30 | import redux.WrapperAction 31 | import styled.styledSvg 32 | import util.drawPentagame 33 | import util.forEach 34 | import kotlin.dom.clear 35 | 36 | interface PentaSvgProps : PentaSvgStateProps, PentaSvgDispatchProps 37 | 38 | class PentaSvg(props: PentaSvgProps) : RComponent(props) { 39 | private val svgRef = createRef() 40 | 41 | // TODO: dispatchSessionEvent 42 | fun dispatchMove(move: PentaMove) { 43 | when (val connection = props.connection) { 44 | is ConnectionState.ConnectedToGame -> { 45 | // when (move) { 46 | // // unsupported by old backend 47 | //// is PentaMove.SelectGrey -> props.dispatchMoveLocal(move) 48 | //// is PentaMove.SelectPlayerPiece -> props.dispatchMoveLocal(move) 49 | // else -> { 50 | GlobalScope.launch { 51 | connection.sendMove(move) 52 | } 53 | // } 54 | // } 55 | } 56 | else -> { 57 | props.dispatchMoveLocal(move) 58 | } 59 | } 60 | } 61 | 62 | override fun RBuilder.render() { 63 | styledSvg { 64 | ref = svgRef 65 | attrs { 66 | attributes["preserveAspectRatio"] = "xMidYMid meet" 67 | } 68 | } 69 | } 70 | 71 | override fun componentDidMount() { 72 | console.log("penta svg mounted") 73 | 74 | redraw(props) 75 | } 76 | 77 | override fun shouldComponentUpdate(nextProps: PentaSvgProps, nextState: RState): Boolean { 78 | // TODO update svg content here 79 | console.log("penta svg updating") 80 | // console.log("props: ${props.boardState}") 81 | // console.log("nextProps: ${nextProps.boardState}") 82 | 83 | // access isPlayBack 84 | when (val conn = nextProps.connection) { 85 | is ConnectionState.ConnectedToGame -> { 86 | if (conn.isPlayback) return false 87 | } 88 | } 89 | redraw(nextProps) 90 | 91 | return false 92 | } 93 | 94 | private fun redraw(svgProps: PentaSvgProps) { 95 | console.debug("drawing...") 96 | 97 | // does SVG stuff 98 | svgRef.current?.let { svg -> 99 | svg.clear() 100 | 101 | val scale = 1000 102 | 103 | val newSVG = SVG.svg { 104 | viewBox = "0 0 $scale $scale" 105 | 106 | drawPentagame(scale, svgProps.boardState, svgProps.connection, svgProps.playingUsers) 107 | } 108 | // val fullSvg = buildString { 109 | // newSVG.render(this, SVG.RenderMode.INLINE) 110 | // } 111 | // console.log("svg: ${fullSvg}") 112 | val svgInline = buildString { 113 | newSVG.children.forEach { 114 | it.render(this, RenderMode.INLINE) 115 | } 116 | } 117 | svg.innerHTML = svgInline 118 | svg.setAttribute("viewBox", newSVG.viewBox!!) 119 | } 120 | 121 | val clickFields = { event: Event -> 122 | event as MouseEvent 123 | console.log("event $event ") 124 | console.log("target ${event.target} ") 125 | when (val target = event.target) { 126 | is SVGCircleElement -> { 127 | target.classList.forEach { id -> 128 | if (PentaBoard.fields.any { it.id == id }) { 129 | // TODO: clickfield 130 | console.log("clicked field $id") 131 | PentagameClick.clickField( 132 | PentaBoard.fields.firstOrNull { it.id == id } 133 | ?: throw IllegalStateException("target '${id}' cannot be found"), 134 | ::dispatchMove, 135 | svgProps.boardState 136 | ) 137 | return@forEach 138 | } 139 | } 140 | } 141 | } 142 | } 143 | val clickPieces = { event: Event -> 144 | event as MouseEvent 145 | console.log("event $event ") 146 | console.log("target ${event.target} ") 147 | when (val target = event.target) { 148 | is Element -> { 149 | target.classList.forEach { id -> 150 | console.log("clicked piece ${id}") 151 | PentagameClick.clickPiece( 152 | props.boardState.figures.firstOrNull { it.id == id } 153 | ?: throw IllegalStateException("target '$id' cannot be found"), 154 | ::dispatchMove, 155 | svgProps.boardState 156 | ) 157 | return@forEach 158 | } 159 | } 160 | } 161 | Unit 162 | } 163 | 164 | PentaBoard.fields.forEach { 165 | svgRef.current?.getElementsByClassName(it.id)?.item(0) 166 | ?.addEventListener("click", clickFields, true) 167 | } 168 | props.boardState.figures.forEach { 169 | svgRef.current?.getElementsByClassName(it.id)?.item(0) 170 | ?.addEventListener("click", clickPieces, true) 171 | } 172 | } 173 | } 174 | 175 | interface PentaSvgParameters : RProps { 176 | 177 | } 178 | 179 | interface PentaSvgStateProps : RProps { 180 | var state: State 181 | var boardState: BoardState 182 | var playingUsers: Map 183 | var connection: ConnectionState 184 | } 185 | 186 | interface PentaSvgDispatchProps : RProps { 187 | var dispatchMoveLocal: (PentaMove) -> Unit 188 | var dispatchConnection: (penta.ConnectionState) -> Unit 189 | } 190 | 191 | val pentaSvgInteractive = 192 | rConnect, WrapperAction, PentaSvgParameters, PentaSvgStateProps, PentaSvgDispatchProps, PentaSvgProps>( 193 | { state, configProps -> 194 | console.debug("PentaViz update state") 195 | console.debug("state:", state) 196 | console.debug("configProps: ", configProps) 197 | this.state = state 198 | boardState = state.boardState 199 | playingUsers = state.playingUsers 200 | connection = state.multiplayerState.connectionState 201 | // todo: trigger redraw here 202 | }, 203 | { dispatch, configProps -> 204 | // any kind of interactivity is linked to dispatching state changes here 205 | console.debug("PentaSvg update dispatch") 206 | console.debug("dispatch: ", dispatch) 207 | console.debug("configProps: ", configProps) 208 | this@rConnect.dispatchMoveLocal = { dispatch(Action(it)) } 209 | this@rConnect.dispatchConnection = { dispatch(Action(it)) } 210 | } 211 | )(PentaSvg::class.js.unsafeCast>()) 212 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/externals/redux-logger.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("redux-logger") 2 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION") 3 | 4 | package externals 5 | 6 | import redux.Middleware 7 | import kotlin.js.* 8 | 9 | external interface ColorsObject { 10 | var title: dynamic /* Boolean | ActionToString */ 11 | get() = definedExternally 12 | set(value) = definedExternally 13 | var prevState: dynamic /* Boolean | StateToString */ 14 | get() = definedExternally 15 | set(value) = definedExternally 16 | var action: dynamic /* Boolean | ActionToString */ 17 | get() = definedExternally 18 | set(value) = definedExternally 19 | var nextState: dynamic /* Boolean | StateToString */ 20 | get() = definedExternally 21 | set(value) = definedExternally 22 | var error: dynamic /* Boolean | ErrorToString */ 23 | get() = definedExternally 24 | set(value) = definedExternally 25 | } 26 | 27 | external interface LevelObject { 28 | var prevState: dynamic /* String | Boolean | StateToString */ 29 | get() = definedExternally 30 | set(value) = definedExternally 31 | var action: dynamic /* String | Boolean | ActionToString */ 32 | get() = definedExternally 33 | set(value) = definedExternally 34 | var nextState: dynamic /* String | Boolean | StateToString */ 35 | get() = definedExternally 36 | set(value) = definedExternally 37 | var error: dynamic /* String | Boolean | ErrorToString */ 38 | get() = definedExternally 39 | set(value) = definedExternally 40 | } 41 | 42 | external interface LogEntryObject { 43 | var action: dynamic /* String | Boolean | ActionToString */ 44 | get() = definedExternally 45 | set(value) = definedExternally 46 | var started: Number? 47 | get() = definedExternally 48 | set(value) = definedExternally 49 | var startedTime: Date? 50 | get() = definedExternally 51 | set(value) = definedExternally 52 | var took: Number? 53 | get() = definedExternally 54 | set(value) = definedExternally 55 | val error: ((error: Any) -> Any)? 56 | get() = definedExternally 57 | val nextState: ((state: Any) -> Any)? 58 | get() = definedExternally 59 | val prevState: ((state: Any) -> Any)? 60 | get() = definedExternally 61 | } 62 | 63 | external interface ReduxLoggerOptions { 64 | var level: dynamic /* String | ActionToString | LevelObject */ 65 | get() = definedExternally 66 | set(value) = definedExternally 67 | var duration: Boolean? 68 | get() = definedExternally 69 | set(value) = definedExternally 70 | var timestamp: Boolean? 71 | get() = definedExternally 72 | set(value) = definedExternally 73 | var colors: dynamic /* ColorsObject | false */ 74 | get() = definedExternally 75 | set(value) = definedExternally 76 | val titleFormatter: ((formattedAction: Any, formattedTime: String, took: Number) -> String)? 77 | get() = definedExternally 78 | var logger: Any? 79 | get() = definedExternally 80 | set(value) = definedExternally 81 | var logErrors: Boolean? 82 | get() = definedExternally 83 | set(value) = definedExternally 84 | var collapsed: dynamic /* Boolean | LoggerPredicate */ 85 | get() = definedExternally 86 | set(value) = definedExternally 87 | var predicate: LoggerPredicate? 88 | get() = definedExternally 89 | set(value) = definedExternally 90 | var diff: Boolean? 91 | get() = definedExternally 92 | set(value) = definedExternally 93 | var diffPredicate: LoggerPredicate? 94 | get() = definedExternally 95 | set(value) = definedExternally 96 | val stateTransformer: ((state: Any) -> Any)? 97 | get() = definedExternally 98 | val actionTransformer: ((action: Any) -> Any)? 99 | get() = definedExternally 100 | val errorTransformer: ((error: Any) -> Any)? 101 | get() = definedExternally 102 | } 103 | 104 | external fun createLogger(options: ReduxLoggerOptions? = definedExternally /* null */): Middleware 105 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/externals/redux-logger_aliases.kt: -------------------------------------------------------------------------------- 1 | package externals 2 | 3 | typealias LoggerPredicate = (getState: () -> Any, action: Any, logEntry: LogEntryObject? /* = null */) -> Boolean 4 | 5 | typealias StateToString = (state: Any) -> String 6 | 7 | typealias ActionToString = (action: Any) -> String 8 | 9 | typealias ErrorToString = (error: Any, prevState: Any) -> String -------------------------------------------------------------------------------- /frontend/src/main/kotlin/externals/redux-logger_impls.kt: -------------------------------------------------------------------------------- 1 | package externals 2 | 3 | data class ReduxLoggerOptionsImpl( 4 | override var level: String /* String | ActionToString | LevelObject */, 5 | override var duration: Boolean? = null, 6 | override var timestamp: Boolean? = null, 7 | override var colors: ColorsObject = ColorsObjectImpl(), /* ColorsObject | false */ 8 | override val titleFormatter: ((formattedAction: Any, formattedTime: String, took: Number) -> String)? = null, 9 | override var logger: Any? = null, 10 | override var logErrors: Boolean? = null, 11 | override var collapsed: Boolean = true, /* Boolean | LoggerPredicate */ 12 | override var predicate: LoggerPredicate? = null, 13 | override var diff: Boolean? = null, 14 | override var diffPredicate: LoggerPredicate? = null, 15 | override val stateTransformer: ((state: Any) -> Any)? = null, 16 | override val actionTransformer: ((action: Any) -> Any)? = null, 17 | override val errorTransformer: ((error: Any) -> Any)? = null 18 | ): ReduxLoggerOptions 19 | 20 | 21 | data class ColorsObjectImpl ( 22 | override var title: Boolean = true, /* Boolean | ActionToString */ 23 | override var prevState: Boolean = true, /* Boolean | StateToString */ 24 | override var action: Boolean = true, /* Boolean | ActionToString */ 25 | override var nextState: Boolean = true, /* Boolean | StateToString */ 26 | override var error: Boolean = true /* Boolean | ErrorToString */ 27 | ): ColorsObject -------------------------------------------------------------------------------- /frontend/src/main/kotlin/reducers/Reducers.kt: -------------------------------------------------------------------------------- 1 | package reducers 2 | 3 | import SessionEvent 4 | import actions.Action 5 | import com.soywiz.klogger.Logger 6 | import io.ktor.http.Url 7 | import penta.BoardState 8 | import penta.BoardState.Companion.processMove 9 | import penta.ConnectionState 10 | import penta.PentaMove 11 | import penta.PlayerIds 12 | import penta.UserInfo 13 | import penta.network.GameEvent 14 | import penta.network.LobbyEvent 15 | import penta.redux.MultiplayerState 16 | import penta.util.exhaustive 17 | import kotlin.browser.document 18 | 19 | data class State( 20 | val boardState: BoardState = BoardState.create(), 21 | val multiplayerState: MultiplayerState = MultiplayerState( 22 | connectionState = ConnectionState.Disconnected(baseUrl = Url(document.location!!.href)) 23 | ), 24 | val playingUsers: Map = mapOf() 25 | // val array: Array = emptyArray() 26 | ) { 27 | fun reduce(action: Any): State { 28 | return when (action) { 29 | is Action<*> -> { 30 | reduce(action.action) 31 | } 32 | is PentaMove -> { 33 | copy(boardState = BoardState.Companion.WithMutableState(boardState).processMove(action)) 34 | } 35 | is GameEvent -> { 36 | val move = action.asMove(boardState) 37 | copy(boardState = BoardState.Companion.WithMutableState(boardState).processMove(move)) 38 | } 39 | is SessionEvent -> { 40 | when(action) { 41 | is SessionEvent.PlayerJoin -> { 42 | val existingUser = playingUsers[action.player] 43 | if (existingUser != null) { 44 | logger.error { "there is already $existingUser on ${action.player}" } 45 | return this 46 | } 47 | copy( 48 | playingUsers = playingUsers + (action.player to action.user) 49 | ) 50 | } 51 | is SessionEvent.PlayerLeave -> { 52 | val existingUser = playingUsers[action.player] 53 | if (existingUser != action.user) { 54 | logger.error { "$existingUser does not match ${action.user} for ${action.player}" } 55 | return this 56 | } 57 | copy ( 58 | playingUsers = playingUsers - action.player 59 | ) 60 | } 61 | is SessionEvent.IllegalMove -> TODO() 62 | is SessionEvent.Undo -> { 63 | copy( 64 | boardState = boardState.reduce(action) 65 | ) 66 | } 67 | is SessionEvent.WrappedGameEvent -> { 68 | copy( 69 | boardState = boardState.reduce(action.event) 70 | ) 71 | } 72 | } 73 | } 74 | is BoardState -> { 75 | // TODO: split into actions 76 | console.warn("should not copy state from action") 77 | copy(boardState = action) 78 | } 79 | is MultiplayerState.Companion.Actions -> { 80 | copy(multiplayerState = multiplayerState.reduce(action)) 81 | } 82 | is LobbyEvent -> { 83 | copy( 84 | multiplayerState = with(multiplayerState) { 85 | copy( 86 | lobby = lobby.reduce(action) 87 | ) 88 | } 89 | ) 90 | } 91 | is ConnectionState -> { 92 | copy( 93 | multiplayerState = multiplayerState.copy( 94 | connectionState = action 95 | ) 96 | ) 97 | } 98 | else -> { 99 | this 100 | } 101 | }.exhaustive 102 | } 103 | 104 | companion object { 105 | private val logger = Logger(this::class.simpleName!!) 106 | val reducer: (State, Any) -> State = State::reduce 107 | } 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/util/CanvasExtensions.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import org.w3c.dom.CanvasRenderingContext2D 4 | import kotlin.math.PI 5 | import kotlin.math.min 6 | 7 | fun CanvasRenderingContext2D.circle(x: Double, y: Double, radius: Double) { 8 | beginPath() 9 | arc(x, y, radius, 0.0, 2 * PI, false) 10 | 11 | // fill?.let { 12 | // context.fill() 13 | // } 14 | // 15 | // stroke?.let { 16 | // context.stroke() 17 | // } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/util/ReactExt.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/util/Util.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import org.w3c.dom.ItemArrayLike 4 | import redux.Reducer 5 | import redux.combineReducers 6 | import kotlin.reflect.KProperty1 7 | 8 | 9 | /** 10 | * Helper function that combines reducers using [combineReducers] where the keys in the map are 11 | * properties of the state object instead of strings with the name of the state's properties 12 | * this helper function has 2 advantages over the original: 13 | * 14 | * 1. It is less error-prone, when you change the name of the property of the state you must change the 15 | * corresponding key or you will get a compile error. 16 | * 2. The compiler is now able to infer the [S] type parameter which means it is no longer needed to provide the 2 type parameters explicitly. 17 | * 18 | * @param S state 19 | * @param A action 20 | * @param R state property type 21 | * 22 | * @param reducers map where the key is the state property and the value is the reducer for said property. 23 | * 24 | * @return the combined reducer. 25 | * 26 | */ 27 | fun combineReducers(reducers: Map, Reducer<*, A>>): Reducer { 28 | return combineReducers(reducers.mapKeys { it.key.name }) 29 | } 30 | 31 | fun ItemArrayLike.forEach(function: (T) -> Unit) { 32 | for (i in 0 until length) { 33 | function(item(i)!!) 34 | } 35 | } -------------------------------------------------------------------------------- /frontend/src/main/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikkyAI/pentagame-react-kt/2c26c2ea122525ba2825f32cbf24bde3c78afa6a/frontend/src/main/resources/favicon.ico -------------------------------------------------------------------------------- /frontend/src/main/resources/float.css: -------------------------------------------------------------------------------- 1 | .absolute { 2 | top: 0; 3 | position: absolute; 4 | } 5 | 6 | .fixed { 7 | left: 0; 8 | top: 0; 9 | position: fixed; 10 | } 11 | 12 | .fixed-background { 13 | left: 0; 14 | top: 0; 15 | position: fixed; 16 | background-color: ghostwhite; 17 | } 18 | 19 | @media (max-aspect-ratio: 1/2) { 20 | .fixed { 21 | height: 100vw; 22 | width: 100vw; 23 | } 24 | .fixed-background { 25 | height: 100vw; 26 | width: 100vw; 27 | } 28 | .absolute { 29 | top: 100vw; 30 | } 31 | } 32 | @media (max-aspect-ratio: 1/1) and (min-aspect-ratio: 1/2) { 33 | .fixed { 34 | height: 50vh; 35 | width: 50vh; 36 | left: calc((100vw - 50vh) / 2); 37 | } 38 | .fixed-background { 39 | height: 50vh; 40 | width: 100vw; 41 | } 42 | .absolute { 43 | top: 50vh; 44 | } 45 | } 46 | @media (max-aspect-ratio: 2/1) and (min-aspect-ratio: 1/1) { 47 | .fixed { 48 | height: 50vw; 49 | width: 50vw; 50 | top: calc((100vh - 50vw) / 2); 51 | } 52 | .fixed-background { 53 | height: 100vh; 54 | width: 50vw; 55 | } 56 | .absolute { 57 | left: 50vw; 58 | } 59 | } 60 | @media (min-aspect-ratio: 2/1) { 61 | .fixed { 62 | height: 100vh; 63 | width: 100vh; 64 | } 65 | .fixed-background { 66 | height: 100vh; 67 | width: 50vh; 68 | } 69 | .absolute { 70 | left: 100vh; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /frontend/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pentagame 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/main/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "pentagame", 3 | "name": "pentagame", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/webpack.config.d/webpack.config.js: -------------------------------------------------------------------------------- 1 | config.devServer = config.devServer || {}; // create devServer in case it is undefined 2 | config.devServer.watchOptions = { 3 | "aggregateTimeout": 5000, 4 | "poll": 1000 5 | }; -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.caching=true 3 | #kotlin.js.experimental.generateKotlinExternals=true 4 | 5 | # will be removed later, but for now it's necessary 6 | refreshVersions.useExperimentalUpdater=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikkyAI/pentagame-react-kt/2c26c2ea122525ba2825f32cbf24bde3c78afa6a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /packages.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.fayard.versions.setupVersionPlaceholdersResolving 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | maven(url = "https://jcenter.bintray.com/") 7 | maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") 8 | } 9 | 10 | resolutionStrategy { 11 | eachPlugin { 12 | val module = when(requested.id.id) { 13 | "kotlinx-serialization" -> "org.jetbrains.kotlin:kotlin-serialization:${requested.version}" 14 | "proguard" -> "net.sf.proguard:proguard-gradle:${requested.version}" 15 | else -> null 16 | } 17 | if(module != null) { 18 | useModule(module) 19 | } 20 | } 21 | } 22 | } 23 | 24 | buildscript { 25 | dependencies.classpath("de.fayard.refreshVersions:de.fayard.refreshVersions.gradle.plugin:0.8.6") 26 | } 27 | 28 | plugins { 29 | id("com.gradle.enterprise").version("3.1.1") 30 | } 31 | 32 | settings.setupVersionPlaceholdersResolving() 33 | 34 | enableFeaturePreview("GRADLE_METADATA") 35 | 36 | //includeBuild("ksvg") 37 | 38 | include("backend") 39 | include("frontend") 40 | include("shared") 41 | include(":muirwik") 42 | //include(":ksvg") 43 | 44 | //project(":ksvg").projectDir = rootDir.resolve("ksvg") 45 | project(":muirwik").projectDir = rootDir.resolve("muirwik/muirwik-components") 46 | 47 | 48 | gradleEnterprise { 49 | buildScan { 50 | termsOfServiceAgree = "yes" 51 | // publishAlwaysIf(true) 52 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 53 | } 54 | } -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile 2 | import java.time.LocalDateTime 3 | import java.time.ZoneOffset 4 | import java.time.format.DateTimeFormatter 5 | 6 | plugins { 7 | kotlin("multiplatform") 8 | id("kotlinx-serialization") 9 | id("de.fayard.dependencies") 10 | } 11 | 12 | val genCommonSrcKt = buildDir.resolve("gen-src/commonMain/kotlin").apply { mkdirs() } 13 | val genBackendResource = buildDir.resolve("gen-src/backendMain/resources").apply { mkdirs() } 14 | 15 | val releaseTime = System.getenv("HEROKU_RELEASE_CREATED_AT") ?: run { 16 | val now = LocalDateTime.now(ZoneOffset.UTC) 17 | DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) 18 | } 19 | 20 | val gitCommitHash = System.getenv("SOURCE_VERSION") ?: captureExec("git", "rev-parse", "HEAD").trim() 21 | 22 | val generateConstantsTask = tasks.create("generateConstants") { 23 | doLast { 24 | logger.lifecycle("generating constants") 25 | generateConstants(genCommonSrcKt, "penta", "Constants") { 26 | field("VERSION") value "0.0.1" 27 | field("GIT_HASH") value gitCommitHash 28 | field("RELEASE_TIME") value releaseTime 29 | } 30 | } 31 | } 32 | val depSize = tasks.create("depSize") 33 | 34 | tasks.withType(AbstractKotlinCompile::class.java).all { 35 | logger.info("registered generating constants to $this") 36 | dependsOn(generateConstantsTask) 37 | } 38 | 39 | kotlin { 40 | jvm() { 41 | compilations.all { 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | } 46 | } 47 | js { 48 | nodejs() 49 | // useCommonJs() 50 | // browser { 51 | // runTask { 52 | // sourceMaps = true 53 | // } 54 | // webpackTask { 55 | // sourceMaps = true 56 | // } 57 | // } 58 | 59 | compilations.all { 60 | kotlinOptions { 61 | sourceMap = true 62 | sourceMapPrefix = "" 63 | metaInfo = true 64 | // moduleKind = "amd" 65 | } 66 | } 67 | } 68 | 69 | /* Targets configuration omitted. 70 | * To find out how to configure the targets, please follow the link: 71 | * https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#setting-up-targets */ 72 | 73 | sourceSets { 74 | val commonMain by getting { 75 | dependencies { 76 | api(kotlin("stdlib")) 77 | api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:_") 78 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:_") 79 | 80 | api(d2v("core")) 81 | api(d2v("color")) 82 | 83 | // logging 84 | api("com.soywiz.korlibs.klogger:klogger:_") 85 | api("io.github.microutils:kotlin-logging-common:_") 86 | 87 | // Redux 88 | api("org.reduxkotlin:redux-kotlin:_") 89 | api("org.reduxkotlin:redux-kotlin-reselect:_") 90 | 91 | // api(project(":ksvg")) 92 | // api("com.github.nwillc:ksvg:3.0.0") 93 | } 94 | 95 | kotlin.srcDirs(genCommonSrcKt.path) 96 | } 97 | 98 | val commonTest by getting { 99 | dependencies { 100 | api(kotlin("test-common")) 101 | api(kotlin("test-annotations-common")) 102 | } 103 | } 104 | 105 | jvm().compilations["main"].defaultSourceSet { 106 | dependencies { 107 | api(kotlin("stdlib-jdk8")) 108 | 109 | api("org.jetbrains.kotlinx:kotlinx-serialization-runtime:_") 110 | 111 | // KTOR 112 | api(ktor("server-core")) 113 | api(ktor("server-cio")) 114 | api(ktor("websockets")) 115 | api(ktor("jackson")) 116 | 117 | // logging 118 | // api("com.soywiz.korlibs.klogger:klogger-jvm:_") 119 | 120 | // serialization 121 | // api("org.jetbrains.kotlinx:kotlinx-serialization-runtime:_") 122 | 123 | // Jackson 124 | api("com.fasterxml.jackson.core:jackson-databind:_") 125 | api("com.fasterxml.jackson.module:jackson-module-kotlin:_") 126 | 127 | // logging 128 | api("ch.qos.logback:logback-classic:_") 129 | api("io.github.microutils:kotlin-logging:_") 130 | } 131 | kotlin.srcDirs(genBackendResource.path) 132 | } 133 | 134 | jvm().compilations["test"].defaultSourceSet { 135 | dependencies { 136 | api(kotlin("test")) 137 | api(kotlin("test-junit")) 138 | } 139 | } 140 | 141 | val commonClient by creating { 142 | dependsOn(commonMain) 143 | dependencies { 144 | api(d2v("viz")) 145 | 146 | api(ktor("client-core")) 147 | api(ktor("client-json")) 148 | api(ktor("client-serialization")) 149 | 150 | // api(ktor("client-websocket")) 151 | } 152 | } 153 | 154 | js().compilations["main"].defaultSourceSet { 155 | dependsOn(commonClient) 156 | dependencies { 157 | // cannot look up serialzation utils otherwise 158 | api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:_") 159 | // logging 160 | api("com.soywiz.korlibs.klogger:klogger-js:_") 161 | api("io.github.microutils:kotlin-logging-js:_") 162 | 163 | // ktor client 164 | api(ktor("client-core-js")) 165 | api(ktor("client-json-js")) 166 | api(ktor("client-serialization-js")) 167 | 168 | api(npm("react", "^16.9.0")) 169 | api(npm("react-dom", "^16.9.0")) 170 | // api(npm("react-router-dom")) 171 | api(npm("styled-components", "^4.4.1")) 172 | api(npm("inline-style-prefixer", "^5.1.0")) 173 | api(npm("core-js", "^3.4.7")) 174 | api(npm("css-in-js-utils", "^3.0.2")) 175 | api(npm("redux", "^4.0.0")) 176 | api(npm("react-redux", "^5.0.7")) 177 | 178 | // temp fix ? 179 | api(npm("text-encoding")) 180 | api(npm("abort-controller")) 181 | 182 | api(npm("redux-logger")) 183 | 184 | val kotlinWrappersVersion = "pre.90-kotlin-1.3.61" 185 | api("org.jetbrains:kotlin-react:16.9.0-${kotlinWrappersVersion}") 186 | api("org.jetbrains:kotlin-react-dom:16.9.0-${kotlinWrappersVersion}") 187 | api("org.jetbrains:kotlin-css:1.0.0-${kotlinWrappersVersion}") 188 | api("org.jetbrains:kotlin-css-js:1.0.0-${kotlinWrappersVersion}") 189 | api("org.jetbrains:kotlin-styled:1.0.0-${kotlinWrappersVersion}") 190 | 191 | api("org.jetbrains:kotlin-redux:4.0.0-${kotlinWrappersVersion}") 192 | api("org.jetbrains:kotlin-react-redux:5.0.7-${kotlinWrappersVersion}") 193 | 194 | // api("org.jetbrains:kotlin-react:_") 195 | // api("org.jetbrains:kotlin-react-dom:_") 196 | // api("org.jetbrains:kotlin-css:_") 197 | // api("org.jetbrains:kotlin-css-js:_") 198 | // api("org.jetbrains:kotlin-styled:_") 199 | 200 | // api("org.jetbrains:kotlin-redux:_") 201 | // api("org.jetbrains:kotlin-react-redux:_") 202 | 203 | // material UI components 204 | api(project(":muirwik")) 205 | // api(project(":muirwik")) 206 | 207 | api("com.github.nwillc:ksvg-js:3.0.0") 208 | } 209 | } 210 | 211 | js().compilations["test"].defaultSourceSet { 212 | dependencies { 213 | // api(kotlin("test")) 214 | } 215 | } 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/client.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.client.HttpClient 2 | import kotlinx.coroutines.CoroutineDispatcher 3 | 4 | expect val client: HttpClient 5 | 6 | expect val clientDispatcher: CoroutineDispatcher 7 | 8 | expect fun showNotification(title: String, body: String) -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/penta/ClientUtil.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import com.github.nwillc.ksvg.elements.SVG 4 | import com.soywiz.klogger.Logger 5 | import io.data2viz.color.Color 6 | import io.data2viz.color.Colors 7 | import io.data2viz.geom.Point 8 | import io.data2viz.math.Angle 9 | import io.data2viz.math.deg 10 | import penta.logic.Piece 11 | import penta.logic.Field 12 | import penta.logic.Field.Start 13 | import kotlin.math.pow 14 | import kotlin.math.sqrt 15 | 16 | fun canClickPiece(clickedPiece: Piece, boardState: BoardState): Boolean { 17 | with(boardState) { 18 | if (winner != null) { 19 | return false 20 | } 21 | if (positions[clickedPiece.id] == null) { 22 | return false 23 | } 24 | // TODO: have multiplayer state in store 25 | when (val state: ConnectionState = ConnectionState.Disconnected()/*penta.client.PentaViz.multiplayerState.value*/) { 26 | is ConnectionState.HasGameSession -> { 27 | if (currentPlayer.id != state.userId) { 28 | return false 29 | } 30 | } 31 | } 32 | if ( 33 | // make sure you are not selecting black or gray 34 | selectedGrayPiece == null && selectedBlackPiece == null && !selectingGrayPiece 35 | && clickedPiece is Piece.Player && currentPlayer == clickedPiece.player 36 | ) { 37 | if (selectedPlayerPiece == null) { 38 | return true 39 | } 40 | if (selectedPlayerPiece == clickedPiece) { 41 | return true 42 | } 43 | } 44 | 45 | if (selectingGrayPiece 46 | && selectedPlayerPiece == null 47 | && clickedPiece is Piece.GrayBlocker 48 | ) { 49 | return true 50 | } 51 | 52 | if (selectedPlayerPiece != null && currentPlayer == selectedPlayerPiece!!.player) { 53 | val playerPiece = selectedPlayerPiece!! 54 | val sourcePos = positions[playerPiece.id] ?: run { 55 | return false 56 | } 57 | val targetPos = positions[clickedPiece.id] ?: return false 58 | if (sourcePos == targetPos) { 59 | return false 60 | } 61 | return true 62 | } 63 | } 64 | return false 65 | } 66 | 67 | fun SVG.drawFigure(figureId: String, center: Point, radius: Double, color: Color, selected: Boolean) { 68 | drawPlayer( 69 | figureId, center, radius, color, null, selected, false 70 | ) 71 | } 72 | 73 | fun SVG.drawPlayer(figureId: String, center: Point, radius: Double, piece: Piece.Player, selected: Boolean, highlight: Boolean, clickable: Boolean) { 74 | drawPlayer( 75 | figureId = figureId, 76 | center = center, 77 | radius = radius, 78 | color = piece.color, 79 | pieceId = if(clickable) piece.id else null, 80 | selected = selected, 81 | highlight = highlight 82 | ) 83 | } 84 | 85 | fun SVG.drawPlayer( 86 | figureId: String, center: Point, radius: Double, color: Color, 87 | pieceId: String?, selected: Boolean, highlight: Boolean 88 | ) { 89 | fun point(angle: Angle, radius: Double, center: Point): Point { 90 | return Point(angle.cos * radius, angle.sin * radius) + center 91 | } 92 | 93 | fun angles(n: Int, start: Angle = 0.deg): List { 94 | val step = 360.deg / n 95 | 96 | return (0..n).map { i -> 97 | (start + (step * i)) 98 | } 99 | } 100 | 101 | val lineWidth = when { 102 | selected -> "3.0" 103 | highlight -> "4.0" 104 | else -> "1.0" 105 | } 106 | val fillColor = when { 107 | selected -> color.brighten(1.0) 108 | else -> color 109 | } 110 | val strokeColor = when { 111 | highlight -> Colors.Web.gray 112 | else -> Colors.Web.black 113 | } 114 | 115 | when (figureId) { 116 | "square" -> { 117 | val points = angles(4, 0.deg).map { angle -> 118 | point(angle, radius, center) 119 | } 120 | 121 | polygon { 122 | if(pieceId != null) { 123 | cssClass = pieceId 124 | } 125 | this.points = points.joinToString(" ") { "${it.x},${it.y}" } 126 | 127 | fill = fillColor.rgbHex 128 | strokeWidth = lineWidth 129 | stroke = strokeColor.rgbHex 130 | } 131 | } 132 | "triangle" -> { 133 | val points = angles(3, -90.deg).map { angle -> 134 | point(angle, radius, center) 135 | } 136 | polygon { 137 | if(pieceId != null) { 138 | cssClass = pieceId 139 | } 140 | this.points = points.joinToString(" ") { "${it.x},${it.y}" } 141 | 142 | fill = fillColor.rgbHex 143 | strokeWidth = lineWidth 144 | stroke = strokeColor.rgbHex 145 | } 146 | } 147 | "cross" -> { 148 | val width = 15 149 | 150 | val p1 = point((45 - width).deg, radius, center) 151 | val p2 = point((45 + width).deg, radius, center) 152 | 153 | val c = sqrt((p2.x - p1.x).pow(2) + (p2.y - p1.y).pow(2)) 154 | 155 | val a = c / sqrt(2.0) 156 | 157 | val points = listOf( 158 | point((45 - width).deg, radius, center), 159 | point((45 + width).deg, radius, center), 160 | point((90).deg, a, center), 161 | point((135 - width).deg, radius, center), 162 | point((135 + width).deg, radius, center), 163 | point((180).deg, a, center), 164 | point((45 + 180 - width).deg, radius, center), 165 | point((45 + 180 + width).deg, radius, center), 166 | point((270).deg, a, center), 167 | point((135 + 180 - width).deg, radius, center), 168 | point((135 + 180 + width).deg, radius, center), 169 | point((360).deg, a, center) 170 | ) 171 | 172 | polygon { 173 | if(pieceId != null) { 174 | cssClass = pieceId 175 | } 176 | this.points = points.joinToString(" ") { "${it.x},${it.y}" } 177 | 178 | fill = fillColor.rgbHex 179 | strokeWidth = lineWidth 180 | stroke = strokeColor.rgbHex 181 | } 182 | } 183 | "circle" -> { 184 | circle { 185 | if(pieceId != null) { 186 | cssClass = pieceId 187 | } 188 | cx = "${center.x}" 189 | cy = "${center.y}" 190 | r = "${radius * 0.8}" 191 | fill = color.rgbHex 192 | strokeWidth = lineWidth 193 | stroke = strokeColor.rgbHex 194 | } 195 | } 196 | else -> throw IllegalStateException("illegal figureId: '$figureId'") 197 | } 198 | } 199 | 200 | 201 | fun cornerPoint(index: Int, angleDelta: Angle = 0.deg, radius: Double = PentaMath.R_): Point { 202 | val angle = (-45 + (index) * 90).deg + angleDelta 203 | 204 | return Point( 205 | radius * angle.cos, 206 | radius * angle.sin 207 | ) / 2 + (Point(0.5, 0.5) * PentaMath.R_) 208 | } 209 | 210 | fun calculatePiecePos(piece: Piece, field: Field?, boardState: BoardState) = with(boardState) { 211 | val logger = Logger(this::class.simpleName!!) 212 | var pos: Point = field?.pos ?: run { 213 | val radius = when (piece) { 214 | is Piece.GrayBlocker -> { 215 | logger.debug {"piece: ${piece.id}"} 216 | logger.debug {"selected: ${selectedGrayPiece?.id}"} 217 | if (selectedGrayPiece == piece) { 218 | val index = players.indexOf(currentPlayer) 219 | val pos = cornerPoint(index, 10.deg, radius = (PentaMath.R_ + (3 * PentaMath.s))) 220 | return@run pos 221 | } 222 | PentaMath.inner_r * -0.2 223 | } 224 | is Piece.BlackBlocker -> { 225 | if (selectedBlackPiece == piece) { 226 | val index = players.indexOf(currentPlayer) 227 | val pos = cornerPoint(index, (-10).deg, radius = (PentaMath.R_ + (3 * PentaMath.s))) 228 | logger.debug {"cornerPos: $pos" } 229 | return@run pos 230 | } 231 | throw IllegalStateException("black piece: $piece cannot be off the board") 232 | } 233 | is Piece.Player -> PentaMath.inner_r * -0.5 234 | // else -> throw NotImplementedError("unhandled piece type: ${piece::class}") 235 | } 236 | val angle = (piece.pentaColor.ordinal * -72.0).deg 237 | 238 | logger.debug { "pentaColor: ${piece.pentaColor.ordinal}" } 239 | 240 | Point( 241 | radius * angle.cos, 242 | radius * angle.sin 243 | ) / 2 + (Point(0.5, 0.5) * PentaMath.R_) 244 | } 245 | if (piece is Piece.Player && field is Start) { 246 | // find all pieces on field and order them 247 | val pieceIds: List = positions.filterValues { it == field }.keys 248 | .sorted() 249 | // find index of piece on field 250 | val pieceNumber = pieceIds.indexOf(piece.id).toDouble() 251 | val angle = 252 | (((field.pentaColor.ordinal * -72.0) + (pieceNumber / pieceIds.size * 360.0) + 360.0) % 360.0).deg 253 | pos = Point( 254 | pos.x + (0.55) * angle.cos, 255 | pos.y + (0.55) * angle.sin 256 | ) 257 | } 258 | if (piece is Piece.Player && field == null) { 259 | // find all pieces on field and order them 260 | val playerPieces = positions.filterValues { it == field }.keys 261 | .map { id -> figures.find { it.id == id }!! } 262 | .filterIsInstance() 263 | .filter { it.pentaColor == piece.pentaColor } 264 | .sortedBy { it.id } 265 | // find index of piece on field 266 | val pieceNumber = playerPieces.indexOf(piece).toDouble() 267 | val angle = 268 | (((piece.pentaColor.ordinal * -72.0) + (pieceNumber / playerPieces.size * 360.0) + 360.0 + 180.0) % 360.0).deg 269 | pos = Point( 270 | pos.x + (0.55) * angle.cos, 271 | pos.y + (0.55) * angle.sin 272 | ) 273 | } 274 | pos 275 | } 276 | -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/penta/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import SessionEvent 4 | import com.soywiz.klogger.Logger 5 | import io.ktor.client.features.websocket.DefaultClientWebSocketSession 6 | import io.ktor.http.Url 7 | import io.ktor.http.cio.websocket.CloseReason 8 | import io.ktor.http.cio.websocket.Frame 9 | import io.ktor.http.cio.websocket.close 10 | import penta.network.GameSessionInfo 11 | import penta.network.GameEvent 12 | import penta.util.json 13 | 14 | sealed class ConnectionState { 15 | companion object { 16 | private val logger = Logger(this::class.simpleName!!) 17 | } 18 | 19 | abstract val baseUrl: Url 20 | abstract val userId: String 21 | 22 | interface NotLoggedIn 23 | 24 | class Unreachable( 25 | override val baseUrl: Url = Url("https://pentagame.herokuapp.com"), 26 | override val userId: String = "" 27 | ) : ConnectionState(), NotLoggedIn 28 | 29 | data class Disconnected( 30 | override val baseUrl: Url = Url("https://pentagame.herokuapp.com"), 31 | override val userId: String = "" 32 | ) : ConnectionState(), NotLoggedIn 33 | 34 | data class UserIDRejected( 35 | override val baseUrl: Url, 36 | override val userId: String, 37 | val reason: String 38 | ) : ConnectionState(), NotLoggedIn 39 | 40 | data class RequiresPassword( 41 | override val baseUrl: Url, 42 | override val userId: String 43 | ) : ConnectionState(), NotLoggedIn 44 | 45 | interface HasSession { 46 | var session: String 47 | } 48 | 49 | interface HasGameSession { 50 | val game: GameSessionInfo 51 | } 52 | 53 | data class Authenticated( 54 | override val baseUrl: Url, 55 | override val userId: String, 56 | override var session: String 57 | ) : ConnectionState(), HasSession 58 | 59 | data class Lobby( 60 | override val baseUrl: Url, 61 | override val userId: String, 62 | override var session: String, 63 | internal val websocketSessionLobby: DefaultClientWebSocketSession 64 | ) : ConnectionState(), HasSession { 65 | suspend fun sendMessage() { 66 | TODO("copy from sendMove") 67 | } 68 | 69 | suspend fun disconnect() { 70 | logger.info { "leaving game" } 71 | logger.info { "sending close frame" } 72 | websocketSessionLobby.close(CloseReason(CloseReason.Codes.NORMAL, "leaving game")) 73 | websocketSessionLobby.terminate() 74 | } 75 | } 76 | 77 | data class ConnectedToGame( 78 | override val baseUrl: Url, 79 | override val userId: String, 80 | override var session: String, 81 | override val game: GameSessionInfo, 82 | val isPlayback: Boolean = false, 83 | private val websocketSessionGame: DefaultClientWebSocketSession, 84 | private val websocketSessionLobby: DefaultClientWebSocketSession 85 | ) : ConnectionState(), HasSession, HasGameSession { 86 | @Deprecated("send session instead") 87 | suspend fun sendMove(move: PentaMove) { 88 | websocketSessionGame.outgoing.send( 89 | Frame.Text(json.stringify(SessionEvent.serializer(), SessionEvent.WrappedGameEvent(move.toSerializable()))) 90 | ) 91 | } 92 | suspend fun sendEvent(event: SessionEvent) { 93 | websocketSessionGame.outgoing.send( 94 | Frame.Text(json.stringify(SessionEvent.serializer(), event)) 95 | ) 96 | } 97 | suspend fun sendSessionEvent(event: SessionEvent) { 98 | websocketSessionGame.outgoing.send( 99 | Frame.Text(json.stringify(SessionEvent.serializer(), event)) 100 | ) 101 | } 102 | 103 | suspend fun leave() { 104 | logger.info { "leaving game" } 105 | // logger.info { "sending close request" } 106 | // running = false 107 | // websocketSession.outgoing.send(Frame.Text("close")) 108 | logger.info { "sending close frame" } 109 | websocketSessionGame.close(CloseReason(CloseReason.Codes.NORMAL, "leaving game")) 110 | websocketSessionGame.terminate() 111 | // logger.info { "finished leaving game" } 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/penta/redux/MultiplayerState.kt: -------------------------------------------------------------------------------- 1 | package penta.redux 2 | 3 | import penta.ConnectionState 4 | import penta.LobbyState 5 | import penta.network.GameSessionInfo 6 | import penta.util.exhaustive 7 | 8 | data class MultiplayerState( 9 | val gameObservers: List = listOf(), 10 | val connectionState: ConnectionState = ConnectionState.Disconnected(), 11 | val lobby: LobbyState = LobbyState() 12 | ) { 13 | fun reduce(action: Actions): MultiplayerState = when(action) { 14 | is Actions.AddObserver -> copy( 15 | gameObservers = gameObservers + action.observer 16 | ) 17 | is Actions.RemoveObserver -> copy( 18 | gameObservers = gameObservers - action.observer 19 | ) 20 | is Actions.SetGames -> copy( 21 | lobby = lobby.copy(games = action.games.associateBy { it.id }) 22 | ) 23 | is Actions.SetConnectionState -> copy( 24 | connectionState = action.connectionState 25 | ) 26 | }.exhaustive 27 | 28 | // fun reduceLobby(action: LobbyEvent): MultiplayerState { 29 | // return copy( 30 | // lobby = lobby.reduce(action) 31 | // ) 32 | // } 33 | 34 | companion object { 35 | sealed class Actions { 36 | data class AddObserver(val observer: String): Actions() 37 | data class RemoveObserver(val observer: String): Actions() 38 | data class SetGames(val games: List): Actions() 39 | data class SetConnectionState(val connectionState: ConnectionState): Actions() 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/penta/test.kt: -------------------------------------------------------------------------------- 1 | val replayGame = """ 2 | [ 3 | { 4 | "type": "penta.network.SerialNotation.InitGame", 5 | "players": [ 6 | "square", 7 | "triangle" 8 | ] 9 | }, 10 | { 11 | "type": "penta.network.SerialNotation.SwapOwnPiece", 12 | "player": "square", 13 | "piece": "p01", 14 | "otherPiece": "p00", 15 | "from": "B", 16 | "to": "A", 17 | "setGrey": false 18 | }, 19 | { 20 | "type": "penta.network.SerialNotation.SwapOwnPiece", 21 | "player": "triangle", 22 | "piece": "p10", 23 | "otherPiece": "p11", 24 | "from": "A", 25 | "to": "B", 26 | "setGrey": false 27 | }, 28 | { 29 | "type": "penta.network.SerialNotation.SwapOwnPiece", 30 | "player": "square", 31 | "piece": "p04", 32 | "otherPiece": "p01", 33 | "from": "E", 34 | "to": "A", 35 | "setGrey": false 36 | }, 37 | { 38 | "type": "penta.network.SerialNotation.SwapOwnPiece", 39 | "player": "triangle", 40 | "piece": "p14", 41 | "otherPiece": "p11", 42 | "from": "E", 43 | "to": "A", 44 | "setGrey": false 45 | }, 46 | { 47 | "type": "penta.network.SerialNotation.MovePlayer", 48 | "player": "square", 49 | "piece": "p01", 50 | "from": "E", 51 | "to": "b", 52 | "setBlack": true, 53 | "setGrey": true 54 | }, 55 | { 56 | "type": "penta.network.SerialNotation.SetBlack", 57 | "id": "b1", 58 | "from": "b", 59 | "to": "E-2-c" 60 | }, 61 | { 62 | "type": "penta.network.SerialNotation.SetGrey", 63 | "id": "g0", 64 | "from": null, 65 | "to": "E-3-c" 66 | }, 67 | { 68 | "type": "penta.network.SerialNotation.MovePlayer", 69 | "player": "triangle", 70 | "piece": "p11", 71 | "from": "E", 72 | "to": "b", 73 | "setBlack": false, 74 | "setGrey": true 75 | }, 76 | { 77 | "type": "penta.network.SerialNotation.SetGrey", 78 | "id": "g1", 79 | "from": null, 80 | "to": "E-4-c" 81 | }, 82 | { 83 | "type": "penta.network.SerialNotation.SwapOwnPiece", 84 | "player": "square", 85 | "piece": "p04", 86 | "otherPiece": "p00", 87 | "from": "A", 88 | "to": "B", 89 | "setGrey": false 90 | }, 91 | { 92 | "type": "penta.network.SerialNotation.SwapOwnPiece", 93 | "player": "triangle", 94 | "piece": "p14", 95 | "otherPiece": "p10", 96 | "from": "A", 97 | "to": "B", 98 | "setGrey": false 99 | }, 100 | { 101 | "type": "penta.network.SerialNotation.MovePlayer", 102 | "player": "square", 103 | "piece": "p00", 104 | "from": "A", 105 | "to": "a", 106 | "setBlack": true, 107 | "setGrey": true 108 | }, 109 | { 110 | "type": "penta.network.SerialNotation.SetBlack", 111 | "id": "b0", 112 | "from": "a", 113 | "to": "c-1-d" 114 | }, 115 | { 116 | "type": "penta.network.SerialNotation.SetGrey", 117 | "id": "g2", 118 | "from": null, 119 | "to": "c-2-d" 120 | }, 121 | { 122 | "type": "penta.network.SerialNotation.MovePlayer", 123 | "player": "triangle", 124 | "piece": "p10", 125 | "from": "A", 126 | "to": "a", 127 | "setBlack": false, 128 | "setGrey": true 129 | }, 130 | { 131 | "type": "penta.network.SerialNotation.SetGrey", 132 | "id": "g3", 133 | "from": null, 134 | "to": "c-3-d" 135 | }, 136 | { 137 | "type": "penta.network.SerialNotation.MovePlayer", 138 | "player": "square", 139 | "piece": "p04", 140 | "from": "B", 141 | "to": "e", 142 | "setBlack": true, 143 | "setGrey": true 144 | }, 145 | { 146 | "type": "penta.network.SerialNotation.SetBlack", 147 | "id": "b4", 148 | "from": "e", 149 | "to": "b-3-c" 150 | }, 151 | { 152 | "type": "penta.network.SerialNotation.SetGrey", 153 | "id": "g4", 154 | "from": null, 155 | "to": "b-2-c" 156 | }, 157 | { 158 | "type": "penta.network.SerialNotation.MovePlayer", 159 | "player": "triangle", 160 | "piece": "p14", 161 | "from": "B", 162 | "to": "e", 163 | "setBlack": false, 164 | "setGrey": true 165 | }, 166 | { 167 | "type": "penta.network.SerialNotation.SetGrey", 168 | "id": "g4", 169 | "from": "b-2-c", 170 | "to": "b-1-c" 171 | }, 172 | { 173 | "type": "penta.network.SerialNotation.Win", 174 | "players": [ 175 | "square", 176 | "triangle" 177 | ] 178 | } 179 | ] 180 | """.trimIndent() 181 | 182 | val replaySetGrey = 183 | """[{"type":"penta.network.SerialNotation.InitGame","players":["triangle","square","cross"]},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"triangle","piece":"p01","otherPiece":"p00","from":"B","to":"A","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"square","piece":"p14","otherPiece":"p13","from":"E","to":"D","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"cross","piece":"p22","otherPiece":"p21","from":"C","to":"B","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"triangle","piece":"p03","otherPiece":"p02","from":"D","to":"C","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"square","piece":"p12","otherPiece":"p11","from":"C","to":"B","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"cross","piece":"p20","otherPiece":"p24","from":"A","to":"E","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"triangle","piece":"p00","otherPiece":"p03","from":"B","to":"C","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"square","piece":"p14","otherPiece":"p11","from":"D","to":"C","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"cross","piece":"p24","otherPiece":"p22","from":"A","to":"B","setGrey":false},{"type":"penta.network.SerialNotation.MovePlayer","player":"triangle","piece":"p00","from":"C","to":"a","setBlack":true,"setGrey":true},{"type":"penta.network.SerialNotation.SetBlack","id":"b0","from":"a","to":"e-3-a"},{"type":"penta.network.SerialNotation.SetGrey","id":"g0","from":null,"to":"e-2-a"},{"type":"penta.network.SerialNotation.MovePlayer","player":"square","piece":"p11","from":"D","to":"b","setBlack":true,"setGrey":true},{"type":"penta.network.SerialNotation.SetBlack","id":"b1","from":"b","to":"b-1-c"},{"type":"penta.network.SerialNotation.SetGrey","id":"g1","from":null,"to":"b-2-c"},{"type":"penta.network.SerialNotation.MovePlayer","player":"cross","piece":"p22","from":"A","to":"c","setBlack":true,"setGrey":true},{"type":"penta.network.SerialNotation.SetBlack","id":"b2","from":"c","to":"b-3-c"},{"type":"penta.network.SerialNotation.SetGrey","id":"g2","from":null,"to":"E-6-c"},{"type":"penta.network.SerialNotation.MovePlayer","player":"triangle","piece":"p03","from":"B","to":"d","setBlack":true,"setGrey":true},{"type":"penta.network.SerialNotation.SetBlack","id":"b3","from":"d","to":"A-2-B"},{"type":"penta.network.SerialNotation.SetGrey","id":"g3","from":null,"to":"A-3-B"},{"type":"penta.network.SerialNotation.MovePlayer","player":"square","piece":"p14","from":"C","to":"e","setBlack":true,"setGrey":true},{"type":"penta.network.SerialNotation.SetBlack","id":"b4","from":"e","to":"e-1-a"},{"type":"penta.network.SerialNotation.SetGrey","id":"g4","from":null,"to":"E-5-c"},{"type":"penta.network.SerialNotation.MovePlayer","player":"cross","piece":"p20","from":"E","to":"b","setBlack":false,"setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"triangle","piece":"p04","otherPiece":"p01","from":"E","to":"A","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"square","piece":"p12","otherPiece":"p10","from":"B","to":"A","setGrey":false},{"type":"penta.network.SerialNotation.SwapOwnPiece","player":"cross","piece":"p21","otherPiece":"p20","from":"C","to":"b","setGrey":false}]""" -------------------------------------------------------------------------------- /shared/src/commonClient/kotlin/penta/util/HttpExt.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.request.HttpRequestBuilder 5 | import io.ktor.client.request.header 6 | import io.ktor.client.request.request 7 | import io.ktor.client.statement.HttpResponse 8 | import io.ktor.client.statement.readText 9 | import io.ktor.http.HttpMethod 10 | import io.ktor.http.HttpStatusCode 11 | import io.ktor.http.Url 12 | import kotlinx.serialization.KSerializer 13 | import kotlinx.serialization.json.Json 14 | import penta.ConnectionState 15 | 16 | suspend fun HttpResponse.parse(serializer: KSerializer, json: Json = penta.util.json): T = json.parse( 17 | serializer, 18 | readText() 19 | ) 20 | 21 | fun HttpRequestBuilder.authenticateWith(state: ConnectionState.HasSession) { 22 | // attributes.put(AttributeKey("credentials"), "include") 23 | header("SESSION", state.session) 24 | } 25 | 26 | suspend fun HttpClient.authenticatedRequest( 27 | url: Url, 28 | state: ConnectionState.HasSession, 29 | method: HttpMethod, 30 | builder: HttpRequestBuilder.() -> Unit = {} 31 | ): HttpResponse { 32 | return request(url) { 33 | this.method = method 34 | authenticateWith(state) 35 | builder() 36 | }.apply { 37 | headers["SESSION"]?.let { 38 | state.session = it 39 | } 40 | check(status == HttpStatusCode.OK) { "response was $status" } 41 | // TODO: handle 403 42 | } 43 | } 44 | 45 | suspend fun HttpClient.authenticatedRequest( 46 | url: Url, 47 | state: ConnectionState.HasSession, 48 | method: HttpMethod, 49 | serializer: KSerializer, 50 | json: Json = penta.util.json, 51 | builder: HttpRequestBuilder.() -> Unit = {} 52 | ): T { 53 | return authenticatedRequest(url, state, method, builder).run { 54 | json.parse( 55 | serializer, 56 | readText() 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/CharUtil.kt: -------------------------------------------------------------------------------- 1 | expect val Char.isUpperCase: Boolean -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/actions/Actions.kt: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | interface ActionHolder { 4 | val action: A 5 | } 6 | 7 | expect class Action(action: A): ActionHolder -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/IllegalMoveException.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | data class IllegalMoveException( 4 | val move: PentaMove.IllegalMove 5 | ): IllegalStateException(move.message) { 6 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/LobbyState.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import com.soywiz.klogger.Logger 4 | import penta.network.GameSessionInfo 5 | import penta.network.LobbyEvent 6 | 7 | data class LobbyState( 8 | val chat: List = listOf(), 9 | val users: List = listOf(), // TODO: use this ? 10 | val games: Map = mapOf() 11 | // TODO: games: update from GameController 12 | ) { 13 | companion object { 14 | private val logger = Logger(this::class.simpleName!!) 15 | } 16 | fun reduce(action: LobbyEvent): LobbyState { 17 | logger.info { "action: $action" } 18 | return when(action) { 19 | is LobbyEvent.InitialSync -> copy( 20 | users = action.users, 21 | chat = action.chat, 22 | games = action.games 23 | ) 24 | is LobbyEvent.UpdateGame -> copy( 25 | games = games + (action.game.id to action.game) 26 | ) 27 | is LobbyEvent.Message -> copy( 28 | chat = chat + action 29 | ) 30 | is LobbyEvent.Join -> copy( 31 | users = users + action.userId 32 | ) 33 | is LobbyEvent.Leave -> copy( 34 | users = users - action.userId 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PentaBoard.kt: -------------------------------------------------------------------------------- 1 | import io.data2viz.geom.Point 2 | import io.data2viz.math.DEG_TO_RAD 3 | import penta.PentaColor 4 | import penta.logic.Field 5 | import penta.logic.Field.ConnectionField 6 | import penta.logic.Field.Start 7 | import penta.logic.Field.Intersection 8 | import penta.logic.Field.Goal 9 | import penta.util.interpolate 10 | import penta.util.length 11 | import kotlin.math.cos 12 | import kotlin.math.sin 13 | 14 | object PentaBoard { 15 | val fields: List 16 | val c: Array 17 | val j: Array 18 | 19 | init { 20 | val corners = PentaColor.values().map { color -> 21 | // val pos = PentaMath.fiveRoots(pentaColor.root) * PentaMath.r 22 | val angle = color.ordinal * -72.0 23 | val pos = Point( 24 | cos(angle * DEG_TO_RAD), 25 | sin(angle * DEG_TO_RAD) 26 | ) * PentaMath.r 27 | // val id = color.ordinal * 2 28 | Start( 29 | // id.toString() 30 | id = "${'A' + color.ordinal}", 31 | pos = pos / 2 + (Point(0.5, 0.5) * PentaMath.R_), 32 | pentaColor = color 33 | ) 34 | } 35 | c = corners.toTypedArray() 36 | val joints = PentaColor.values().map { color -> 37 | // val pos = PentaMath.fiveRoots(pentaColor.root) * -PentaMath.inner_r 38 | // val id = ((color.ordinal + 2) % 5 * 2) + 1 39 | val angle = color.ordinal * -72.0 40 | val pos = Point( 41 | cos(angle * DEG_TO_RAD), 42 | sin(angle * DEG_TO_RAD) 43 | ) * -PentaMath.inner_r 44 | Goal( 45 | id = "${'a' + color.ordinal}", 46 | // id.toString(), 47 | pos = pos / 2 + (Point(0.5, 0.5) * PentaMath.R_), 48 | pentaColor = color 49 | ) 50 | } 51 | j = joints.toTypedArray() 52 | val outerSteps = 3 53 | var angle = 0.0 54 | val outerRing = (corners + corners.first()).zipWithNext { current, next -> 55 | current.connectIntersection(next) 56 | // val interpolatedColors = current.color.interpolate(next.color, outerSteps) 57 | val connectingNodes = (0 until outerSteps).map { i -> 58 | angle -= 72.0 / 4 59 | val pos = Point( 60 | (PentaMath.r * cos(angle * DEG_TO_RAD)), 61 | (PentaMath.r * sin(angle * DEG_TO_RAD)) 62 | ) 63 | // TODO: connect 64 | ConnectionField( 65 | id = "${current.id}-${i + 1}-${next.id}", 66 | altId = "${next.id}-${outerSteps - i}-${current.id}", 67 | pos = pos / 2 + (Point(0.5, 0.5) * PentaMath.R_) 68 | // color = interpolatedColors[i] 69 | ) 70 | } 71 | angle -= 72.0 / 4 72 | current.connect(connectingNodes.first()) 73 | connectingNodes.zipWithNext { currentNode, nextNode -> 74 | currentNode.connect(nextNode) 75 | } 76 | connectingNodes.last().connect(next) 77 | connectingNodes 78 | }.flatten() 79 | val innerRing = (joints + joints.first()).zipWithNext { current, next -> 80 | connect(current, next, 3) 81 | }.flatten() 82 | val interConnections = joints.mapIndexed { index, current -> 83 | connect(corners[(index + 2) % corners.size], current, steps = 6) + 84 | connect(corners[(index + 3) % corners.size], current, steps = 6) 85 | }.flatten() 86 | fields = corners + joints + outerRing + innerRing + interConnections 87 | } 88 | 89 | // lookup helper 90 | private val fieldMap = fields.associateBy { it.id } + 91 | fields.filterIsInstance().associateBy { it.altId } 92 | 93 | operator fun get(id: String) = fieldMap[id] 94 | 95 | private fun connect(current: Intersection, next: Intersection, steps: Int): List { 96 | current.connectIntersection(next) 97 | val interpolatedPos = current.pos.interpolate( 98 | next.pos, steps, 99 | skip = current.radius + (PentaMath.s / 2) 100 | ) 101 | // val interpolatedColors = current.color.interpolate(next.color, steps) 102 | // println("connecting colors: ${interpolatedColors.size}") 103 | val connectingNodes = (0 until steps).map { i -> 104 | val pos = interpolatedPos[i] 105 | ConnectionField( 106 | id = "${current.id}-${i + 1}-${next.id}", 107 | altId = "${next.id}-${steps - i}-${current.id}", 108 | pos = pos 109 | // color = interpolatedColors[i] 110 | ) 111 | } 112 | current.connect(connectingNodes.first()) 113 | connectingNodes.zipWithNext { currentNode, nextNode -> 114 | currentNode.connect(nextNode) 115 | } 116 | connectingNodes.last().connect(next) 117 | return connectingNodes 118 | } 119 | 120 | fun findFieldAtPos(mousePos: Point): Field? = fields.find { 121 | (it.pos - mousePos).length < it.radius 122 | } 123 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PentaColor.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import io.data2viz.color.Color 4 | import io.data2viz.color.Colors 5 | import io.data2viz.color.col 6 | 7 | enum class PentaColor(val label: String, val color: Color) { //}, val root: penta.math.Point) { 8 | A("white", 0xf6f6f6.col),//, penta.math.Point(+1.0, 0.0)), #f6f6f6 9 | B("blue", Colors.Web.dodgerblue),//, penta.math.Point(+1.0, +1.0)), 10 | C("red", Colors.Web.red),//, penta.math.Point(-1.0, +1.0)), 11 | D("yellow", Colors.Web.yellow),//, penta.math.Point(-1.0, -1.0)), 12 | E("green", Colors.Web.forestgreen)//, penta.math.Point(+1.0, -1.0)); 13 | } 14 | 15 | object PentaColors { 16 | val FOREGROUND: Color = "#d3d3d3".col 17 | val BACKGROUND: Color = "#28292b".col 18 | val BLACK: Color = Colors.Web.black 19 | val GREY: Color = Colors.Web.grey 20 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PentaMath.kt: -------------------------------------------------------------------------------- 1 | import io.data2viz.geom.Point 2 | import kotlin.math.sqrt 3 | 4 | object PentaMath { 5 | val PHI = (sqrt(5.0) + 1.0) / 2.0 6 | // distances 7 | const val k = 3 8 | const val l = 6 9 | // diameter of unit 10 | const val s = 1.0 11 | // diameter 12 | val c = sqrt(5.0) // (2.0/PHI) + 1.0 13 | // diameters 14 | val j = (9.0 - (2.0 * sqrt(5.0))) / sqrt(5.0) // (c+9.0+sqrt(5.0))/sqrt(5.0) 15 | val L = c + 12 + j 16 | val K = (2.0 * j) + 6 17 | val d = 2.0 * (c + 12 + j) + (2 * j + 6) // (2.0*L) + K 18 | 19 | // diameter of outer ring 20 | val r = (2.0 / 5.0) * sqrt(1570 + (698.0 * sqrt(5.0)))// d / sqrt(PHI + 2.0) 21 | // diameter with 22 | val R_ = r + c 23 | val inner_r = ((k + j) * 2 * (1.0 + sqrt(5.0))) / sqrt(2.0 * (5.0 + sqrt(5.0))) 24 | 25 | fun fiveRoots(p: Point): Point { 26 | if (p.y == 0.0) return p 27 | return Point( 28 | ((p.x * sqrt(5.0) - 1.0) / 4.0), p.y 29 | * sqrt((5.0 + (p.x * sqrt(5.0))) / 8.0) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PentaMove.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import penta.logic.Field 4 | import penta.logic.GameType 5 | import penta.logic.Piece 6 | import penta.network.GameEvent 7 | 8 | sealed class PentaMove { 9 | abstract fun toSerializable(): GameEvent 10 | abstract fun asNotation(): String // TODO: move notation serializer/parser to GameEvent 11 | 12 | interface Move { 13 | val playerPiece: Piece.Player 14 | val from: Field 15 | val to: Field 16 | } 17 | 18 | interface Swap : Move { 19 | val otherPlayerPiece: Piece.Player 20 | } 21 | 22 | interface CanSetBlack 23 | interface CanSetGrey 24 | 25 | // -> 26 | data class MovePlayer( 27 | override val playerPiece: Piece.Player, 28 | override val from: Field, 29 | override val to: Field 30 | ) : PentaMove(), Move, CanSetBlack, CanSetGrey { 31 | override fun asNotation(): String = "${playerPiece.player}: ${playerPiece.id} (${from.id} -> ${to.id})" 32 | override fun toSerializable() = 33 | GameEvent.MovePlayer( 34 | player = playerPiece.player, 35 | piece = playerPiece.id, 36 | from = from.id, 37 | to = to.id 38 | ) 39 | } 40 | 41 | // -> 42 | data class ForcedPlayerMove( 43 | override val playerPiece: Piece.Player, 44 | override val from: Field, 45 | override val to: Field 46 | ) : PentaMove(), Move, CanSetGrey { 47 | override fun asNotation(): String = "${playerPiece.player}: ${playerPiece.id} (${from.id} -> ${to.id})" 48 | override fun toSerializable() = 49 | GameEvent.ForcedMovePlayer( 50 | player = playerPiece.player, 51 | piece = playerPiece.id, 52 | from = from.id, 53 | to = to.id 54 | ) 55 | } 56 | 57 | // <-> 58 | data class SwapOwnPiece( 59 | override val playerPiece: Piece.Player, 60 | override val otherPlayerPiece: Piece.Player, 61 | override val from: Field, 62 | override val to: Field 63 | ) : PentaMove(), Swap, CanSetGrey { 64 | override fun asNotation(): String = 65 | "${playerPiece.player}: ${playerPiece.id} ${otherPlayerPiece.id}{${otherPlayerPiece.player}} (${from.id} <-> ${to.id})" 66 | 67 | override fun toSerializable() = 68 | GameEvent.SwapOwnPiece( 69 | player = playerPiece.player, 70 | piece = playerPiece.id, 71 | otherPiece = otherPlayerPiece.id, 72 | from = from.id, 73 | to = to.id 74 | ) 75 | } 76 | 77 | // <-/-> 78 | data class SwapHostilePieces( 79 | override val playerPiece: Piece.Player, 80 | override val otherPlayerPiece: Piece.Player, 81 | override val from: Field, 82 | override val to: Field 83 | ) : PentaMove(), Swap, CanSetGrey { 84 | override fun asNotation(): String = 85 | "${playerPiece.player}: ${playerPiece.id} ${otherPlayerPiece.id}{${otherPlayerPiece.player}} (${from.id} <+> ${to.id})" 86 | 87 | override fun toSerializable() = 88 | GameEvent.SwapHostilePieces( 89 | player = playerPiece.player, 90 | otherPlayer = otherPlayerPiece.player, 91 | piece = playerPiece.id, 92 | otherPiece = otherPlayerPiece.id, 93 | from = from.id, 94 | to = to.id 95 | ) 96 | } 97 | 98 | // <=> 99 | data class CooperativeSwap( 100 | override val playerPiece: Piece.Player, 101 | override val otherPlayerPiece: Piece.Player, 102 | override val from: Field, 103 | override val to: Field 104 | ) : PentaMove(), Swap, CanSetGrey { 105 | override fun asNotation(): String = 106 | "${playerPiece.player}: ${playerPiece.id} ${otherPlayerPiece.id}{${otherPlayerPiece.player}} (${from.id} <=> ${to.id})" 107 | 108 | override fun toSerializable() = 109 | GameEvent.CooperativeSwap( 110 | player = playerPiece.player, 111 | otherPlayer = otherPlayerPiece.player, 112 | piece = playerPiece.id, 113 | otherPiece = otherPlayerPiece.id, 114 | from = from.id, 115 | to = to.id 116 | ) 117 | } 118 | 119 | data class SetBlack( 120 | val piece: Piece.BlackBlocker, 121 | val from: Field, 122 | val to: Field 123 | ) : PentaMove() { 124 | override fun asNotation(): String = "& [${to.id}]" 125 | override fun toSerializable() = GameEvent.SetBlack(piece.id, from.id, to.id) 126 | } 127 | 128 | data class SetGrey( 129 | val piece: Piece.GrayBlocker, 130 | val from: Field?, 131 | val to: Field 132 | ) : PentaMove() { 133 | override fun asNotation(): String = "& [${to.id}]" 134 | override fun toSerializable() = GameEvent.SetGrey(piece.id, from?.id, to.id) 135 | } 136 | 137 | data class SelectGrey( 138 | val from: Field, 139 | // val before: Piece.GrayBlocker?, 140 | val grayPiece: Piece.GrayBlocker? 141 | ) : PentaMove() { 142 | override fun asNotation(): String = "select grey ${grayPiece?.id}" 143 | override fun toSerializable(): GameEvent = GameEvent.SelectGrey( 144 | from = from.id, 145 | // before = before?.id, 146 | id = grayPiece?.id 147 | ) 148 | } 149 | 150 | data class SelectPlayerPiece( 151 | val before: Piece.Player?, 152 | val playerPiece: Piece.Player? 153 | ) : PentaMove() { 154 | override fun asNotation(): String = "select player ${playerPiece?.id}" 155 | override fun toSerializable(): GameEvent = GameEvent.SelectPlayerPiece(before?.id, playerPiece?.id) 156 | } 157 | 158 | // data class PlayerJoin(val player: PlayerState) : PentaMove() { 159 | // override fun asNotation(): String = ">>> [${player.id}]" 160 | // override fun toSerializable() = GameEvent.PlayerJoin(player) 161 | // } 162 | 163 | // @Deprecated("not a move") 164 | // data class ObserverJoin(val id: String) : PentaMove() { 165 | // override fun asNotation(): String = "join [${id}]" 166 | // override fun toSerializable() = GameEvent.ObserverJoin(id) 167 | // } 168 | // 169 | // @Deprecated("not a move") 170 | // data class ObserverLeave(val id: String) : PentaMove() { 171 | // override fun asNotation(): String = "leave [${id}]" 172 | // override fun toSerializable() = GameEvent.ObserverLeave(id) 173 | // } 174 | 175 | data class SetGameType( 176 | val gameType: GameType 177 | ): PentaMove() { 178 | override fun asNotation(): String = "chgametpe $gameType" 179 | override fun toSerializable() = GameEvent.SetGameType( 180 | gameType = gameType 181 | ) 182 | } 183 | 184 | // TODO: also initialize player count / gamemode 185 | object InitGame: PentaMove() { 186 | override fun asNotation(): String = ">>> " 187 | override fun toSerializable() = GameEvent.InitGame 188 | } 189 | 190 | // is this a move ? 191 | data class Win(val players: List) : PentaMove() { 192 | override fun asNotation(): String = "winner: ${players.joinToString(" & ")}" 193 | override fun toSerializable() = GameEvent.Win(players) 194 | } 195 | 196 | // TODO: session specific 197 | data class IllegalMove(val message: String, val move: PentaMove) : PentaMove() { 198 | override fun asNotation(): String { 199 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 200 | } 201 | 202 | override fun toSerializable() = GameEvent.IllegalMove(message, move.toSerializable()) 203 | } 204 | 205 | data class Undo(val moves: List) : PentaMove() { 206 | override fun asNotation(): String = "UNDO ${moves.map { it }}" 207 | override fun toSerializable(): GameEvent = GameEvent.Undo( 208 | moves.map { it } 209 | ) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PlayerFaces.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | @Deprecated("old code?") 4 | enum class PlayerFaces { 5 | TRIANGLE, 6 | SQUARE, 7 | CROSS, 8 | CIRCLE 9 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/PlayerIds.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import kotlinx.serialization.Decoder 4 | import kotlinx.serialization.Encoder 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.PrimitiveDescriptor 7 | import kotlinx.serialization.PrimitiveKind 8 | import kotlinx.serialization.SerialDescriptor 9 | import kotlinx.serialization.Serializable 10 | 11 | // TODO: switch to enum ? 12 | @Serializable(with = PlayerIds.Companion::class) 13 | enum class PlayerIds { 14 | PLAYER_1, PLAYER_2, PLAYER_3, PLAYER_4; 15 | 16 | val id: String 17 | get() = this.name 18 | 19 | companion object : KSerializer { 20 | override val descriptor: SerialDescriptor 21 | get() = PrimitiveDescriptor("PlayerIds", PrimitiveKind.STRING) // SerialDescriptor("PlayerIds", kind = StructureKind.OBJECT) 22 | 23 | override fun deserialize(decoder: Decoder): PlayerIds { 24 | return valueOf(decoder.decodeString()) 25 | } 26 | 27 | override fun serialize(encoder: Encoder, value: PlayerIds) { 28 | encoder.encodeString(value.name) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/UserInfo.kt: -------------------------------------------------------------------------------- 1 | package penta 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class UserInfo( 7 | val userId: String, 8 | var figureId: String // TODO: turn into enum or sealed class 9 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/logic/Field.kt: -------------------------------------------------------------------------------- 1 | package penta.logic 2 | 3 | import io.data2viz.color.Color 4 | import io.data2viz.geom.Point 5 | import penta.PentaColor 6 | import penta.PentaColors 7 | 8 | sealed class Field { 9 | abstract val id: String 10 | abstract val pos: Point 11 | abstract val radius: Double 12 | abstract val color: Color 13 | 14 | protected var connectedFields: List = listOf() 15 | fun connect(vararg others: Field) { 16 | connectedFields += others 17 | others.forEach { 18 | it.connectedFields += this 19 | } 20 | } 21 | 22 | open val connected: List 23 | get() = connectedFields 24 | 25 | abstract class Intersection : Field() { 26 | abstract val pentaColor: PentaColor 27 | 28 | override val color: Color get() = pentaColor.color 29 | 30 | protected var connectedIntersectionFields: List = listOf() 31 | fun connectIntersection(vararg others: Intersection) { 32 | connectedIntersectionFields += others 33 | others.forEach { 34 | it.connectedIntersectionFields += this 35 | } 36 | } 37 | 38 | open val connectedIntersections: List 39 | get() = connectedIntersectionFields 40 | } 41 | data class ConnectionField( 42 | override val id: String, 43 | val altId: String, 44 | override val pos: Point 45 | ) : Field() { 46 | override val color: Color = PentaColors.FOREGROUND 47 | override val radius: Double = PentaMath.s / 2 48 | } 49 | 50 | data class Goal( 51 | override val id: String, 52 | override val pos: Point, 53 | override val pentaColor: PentaColor 54 | ) : Intersection() { 55 | override val radius: Double = PentaMath.j / 2 56 | override val connected: List 57 | get() = connectedFields 58 | } 59 | 60 | 61 | data class Start( 62 | override val id: String, 63 | override val pos: Point, 64 | override val pentaColor: PentaColor 65 | ) : Intersection() { 66 | override val radius: Double = PentaMath.c / 2 67 | override val connected: List 68 | get() = connectedFields 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/logic/GameType.kt: -------------------------------------------------------------------------------- 1 | package penta.logic 2 | 3 | import penta.PlayerIds 4 | 5 | enum class GameType(val teams: Map>) { 6 | TWO(mapOf( 7 | 1 to listOf(PlayerIds.PLAYER_1), 8 | 2 to listOf(PlayerIds.PLAYER_2) 9 | )), 10 | THREE(mapOf( 11 | 1 to listOf(PlayerIds.PLAYER_1), 12 | 2 to listOf(PlayerIds.PLAYER_2), 13 | 3 to listOf(PlayerIds.PLAYER_3) 14 | )), 15 | FOUR(mapOf( 16 | 1 to listOf(PlayerIds.PLAYER_1), 17 | 2 to listOf(PlayerIds.PLAYER_2), 18 | 3 to listOf(PlayerIds.PLAYER_3), 19 | 4 to listOf(PlayerIds.PLAYER_4) 20 | )), 21 | TWO_VS_TO(mapOf( 22 | 1 to listOf(PlayerIds.PLAYER_1, PlayerIds.PLAYER_3), 23 | 2 to listOf(PlayerIds.PLAYER_2, PlayerIds.PLAYER_4) 24 | )); 25 | val playerCount: Int = teams.values.flatten().size 26 | val players: List = teams.values.flatten().sortedBy { it.ordinal } 27 | } 28 | 29 | // TODO: replace with data class ? 30 | // example: 31 | // playerCount: 3 32 | // teams: [[1], [2], [3]] -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/logic/Piece.kt: -------------------------------------------------------------------------------- 1 | package penta.logic 2 | 3 | import io.data2viz.color.Color 4 | import penta.PentaColor 5 | import penta.PentaColors 6 | import penta.PlayerIds 7 | import penta.logic.Field.Goal 8 | 9 | sealed class Piece { 10 | abstract val id: String 11 | abstract val pentaColor: PentaColor 12 | abstract val radius: Double 13 | 14 | open val color: Color get() = pentaColor.color 15 | 16 | interface Blocker { 17 | companion object { 18 | const val RADIUS = PentaMath.s / 2.5 19 | } 20 | 21 | } 22 | 23 | data class BlackBlocker( 24 | override val id: String, 25 | override val pentaColor: PentaColor, 26 | val originalPosition: Goal 27 | ) : Piece(), Blocker { 28 | override val radius get() = Blocker.RADIUS 29 | override val color: Color get() = PentaColors.BLACK 30 | } 31 | 32 | data class GrayBlocker( 33 | override val id: String, 34 | override val pentaColor: PentaColor 35 | ) : Piece(), Blocker { 36 | override val radius get() = Blocker.RADIUS 37 | override val color: Color get() = PentaColors.GREY 38 | } 39 | 40 | data class Player( 41 | override val id: String, 42 | val player: PlayerIds, 43 | // val figureId: String, 44 | override val pentaColor: PentaColor 45 | ) : Piece() { 46 | override val radius: Double get() = RADIUS 47 | override val color: Color get() = pentaColor.color.brighten(0.5) 48 | companion object { 49 | const val RADIUS = PentaMath.s / 2.3 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/GameEvent.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import PentaBoard 4 | import kotlinx.serialization.Polymorphic 5 | import kotlinx.serialization.PolymorphicSerializer 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.modules.SerializersModuleBuilder 8 | import penta.BoardState 9 | import penta.PentaMove 10 | import penta.PlayerIds 11 | import penta.logic.Piece 12 | import penta.logic.GameType 13 | 14 | @Polymorphic 15 | @Serializable(PolymorphicSerializer::class) 16 | sealed class GameEvent { 17 | // TODO: move into abstract class SerializedMove 18 | abstract fun asMove(boardState: BoardState): PentaMove 19 | 20 | @Serializable 21 | data class MovePlayer( 22 | val player: PlayerIds, 23 | val piece: String, 24 | val from: String, 25 | val to: String 26 | ) : GameEvent() { 27 | override fun asMove(boardState: BoardState) = 28 | PentaMove.MovePlayer( 29 | playerPiece = boardState.figures.filterIsInstance() 30 | .first { it.player == player && it.id == piece }, 31 | from = PentaBoard.get(from)!!, 32 | to = PentaBoard.get(to)!! 33 | ) 34 | } 35 | 36 | @Serializable 37 | data class ForcedMovePlayer( 38 | val player: PlayerIds, 39 | val piece: String, 40 | val from: String, 41 | val to: String 42 | ) : GameEvent() { 43 | override fun asMove(boardState: BoardState) = 44 | PentaMove.ForcedPlayerMove( 45 | playerPiece = boardState.figures.filterIsInstance() 46 | .first { it.player == player && it.id == piece }, 47 | from = PentaBoard.get(from)!!, 48 | to = PentaBoard.get(to)!! 49 | ) 50 | } 51 | 52 | @Serializable 53 | data class SwapOwnPiece( 54 | val player: PlayerIds, 55 | val piece: String, 56 | val otherPiece: String, 57 | val from: String, 58 | val to: String 59 | ) : GameEvent() { 60 | override fun asMove(boardState: BoardState) = 61 | PentaMove.SwapOwnPiece( 62 | playerPiece = boardState.figures.filterIsInstance() 63 | .first { it.player == player && it.id == piece }, 64 | otherPlayerPiece = boardState.figures.filterIsInstance() 65 | .first { it.player == player && it.id == otherPiece }, 66 | from = PentaBoard[from]!!, 67 | to = PentaBoard[to]!! 68 | ) 69 | } 70 | 71 | @Serializable 72 | data class SwapHostilePieces( 73 | val player: PlayerIds, 74 | val otherPlayer: PlayerIds, 75 | val piece: String, 76 | val otherPiece: String, 77 | val from: String, 78 | val to: String 79 | ) : GameEvent() { 80 | override fun asMove(boardState: BoardState): PentaMove = 81 | PentaMove.SwapHostilePieces( 82 | playerPiece = boardState.figures.filterIsInstance() 83 | .first { it.player == player && it.id == piece }, 84 | otherPlayerPiece = boardState.figures.filterIsInstance() 85 | .first { it.player == otherPlayer && it.id == otherPiece }, 86 | from = PentaBoard.get(from)!!, 87 | to = PentaBoard.get(to)!! 88 | ) 89 | } 90 | 91 | @Serializable 92 | data class CooperativeSwap( 93 | val player: PlayerIds, 94 | val otherPlayer: PlayerIds, 95 | val piece: String, 96 | val otherPiece: String, 97 | val from: String, 98 | val to: String 99 | ) : GameEvent() { 100 | override fun asMove(boardState: BoardState): PentaMove { 101 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 102 | } 103 | } 104 | 105 | @Serializable 106 | data class SetGrey( 107 | val id: String, 108 | val from: String?, 109 | val to: String 110 | ) : GameEvent() { 111 | override fun asMove(boardState: BoardState) = 112 | PentaMove.SetGrey( 113 | piece = boardState.figures.filterIsInstance().first { it.id == id }, 114 | from = from?.let { PentaBoard.get(it) }, 115 | to = PentaBoard.get(to)!! 116 | ) 117 | } 118 | 119 | @Serializable 120 | data class SetBlack( 121 | val id: String, 122 | val from: String, 123 | val to: String 124 | ) : GameEvent() { 125 | override fun asMove(boardState: BoardState) = 126 | PentaMove.SetBlack( 127 | piece = boardState.figures.filterIsInstance().first { it.id == id }, 128 | from = PentaBoard.get(from)!!, 129 | to = PentaBoard.get(to)!! 130 | ) 131 | } 132 | 133 | @Serializable 134 | data class SelectGrey( 135 | val from: String, 136 | // val before: String?, 137 | val id: String? 138 | ) : GameEvent() { 139 | override fun asMove(boardState: BoardState) = 140 | PentaMove.SelectGrey( 141 | from = PentaBoard.get(from)!!, 142 | // before = before?.let { boardState.figures.filterIsInstance().first { it.id == before } }, 143 | grayPiece = id?.let { boardState.figures.filterIsInstance().first { p -> p.id == it } } 144 | ) 145 | } 146 | 147 | @Serializable 148 | data class SelectPlayerPiece( 149 | val before: String?, 150 | val id: String? 151 | ) : GameEvent() { 152 | override fun asMove(boardState: BoardState) = 153 | PentaMove.SelectPlayerPiece( 154 | before = before?.let { boardState.figures.filterIsInstance().first { p -> p.id == it } }, 155 | playerPiece = id?.let { boardState.figures.filterIsInstance().first { p -> p.id == it } } 156 | ) 157 | } 158 | 159 | // @Deprecated("move logic to SessionEvent") 160 | // @Serializable 161 | // data class PlayerJoin( 162 | // val player: PlayerState 163 | // ) : GameEvent() { 164 | // override fun asMove(boardState: BoardState) = 165 | // PentaMove.PlayerJoin( 166 | // player = player 167 | // ) 168 | // } 169 | 170 | // TODO: also initialize player count / gamemode 171 | @Serializable 172 | data class SetGameType( 173 | val gameType: GameType 174 | ) : GameEvent() { 175 | override fun asMove(boardState: BoardState) = 176 | PentaMove.SetGameType( 177 | gameType = gameType 178 | ) 179 | } 180 | 181 | @Serializable 182 | object InitGame: GameEvent() { 183 | override fun asMove(boardState: BoardState) = PentaMove.InitGame 184 | } 185 | 186 | @Serializable 187 | data class Win( 188 | val players: List 189 | ) : GameEvent() { 190 | override fun asMove(boardState: BoardState) = PentaMove.Win( 191 | players = players 192 | ) 193 | } 194 | 195 | @Deprecated("move logic to SessionEvent") 196 | @Serializable 197 | data class IllegalMove( 198 | val message: String, 199 | val move: GameEvent 200 | ) : GameEvent() { 201 | override fun asMove(boardState: BoardState): PentaMove { 202 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 203 | } 204 | } 205 | 206 | // TODO: move to SessionEvents 207 | @Serializable 208 | data class Undo( 209 | val moves: List 210 | ) : GameEvent() { 211 | override fun asMove(boardState: BoardState): PentaMove = PentaMove.Undo( 212 | moves = moves.map { it } 213 | ) 214 | } 215 | 216 | companion object { 217 | fun install(builder: SerializersModuleBuilder) { 218 | builder.polymorphic { 219 | MovePlayer::class with MovePlayer.serializer() 220 | ForcedMovePlayer::class with ForcedMovePlayer.serializer() 221 | SwapOwnPiece::class with SwapOwnPiece.serializer() 222 | SwapHostilePieces::class with SwapHostilePieces.serializer() 223 | CooperativeSwap::class with CooperativeSwap.serializer() 224 | SetBlack::class with SetBlack.serializer() 225 | SetGrey::class with SetGrey.serializer() 226 | SelectGrey::class with SelectGrey.serializer() 227 | SelectPlayerPiece::class with SelectPlayerPiece.serializer() 228 | // PlayerJoin::class with PlayerJoin.serializer() 229 | SetGameType::class with SetGameType.serializer() 230 | InitGame::class with InitGame.serializer() 231 | Win::class with Win.serializer() 232 | IllegalMove::class with IllegalMove.serializer() 233 | Undo::class with Undo.serializer() 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/GameSessionInfo.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import kotlinx.serialization.Serializable 4 | import penta.PlayerIds 5 | import penta.UserInfo 6 | 7 | @Serializable 8 | data class GameSessionInfo( 9 | val id: String, 10 | val owner: String, 11 | val running: Boolean, 12 | val playingUsers: Map, 13 | val observers: List 14 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/LobbyEvent.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import kotlinx.serialization.Polymorphic 4 | import kotlinx.serialization.PolymorphicSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.modules.SerializersModuleBuilder 7 | import penta.BoardState 8 | 9 | @Polymorphic 10 | @Serializable(PolymorphicSerializer::class) 11 | sealed class LobbyEvent { 12 | interface FromClient 13 | interface FromServer 14 | 15 | // TODO: add more message types to update and remove games 16 | @Serializable 17 | data class UpdateGame( 18 | val game: GameSessionInfo 19 | ): LobbyEvent(), FromServer 20 | 21 | @Serializable 22 | data class Message( 23 | val userId: String, 24 | val content: String 25 | ): LobbyEvent(), FromClient, FromServer 26 | 27 | @Serializable 28 | data class Join( 29 | val userId: String 30 | ): LobbyEvent(), FromServer 31 | 32 | @Serializable 33 | data class Leave( 34 | val userId: String, 35 | val reason: String 36 | ): LobbyEvent(), FromServer 37 | 38 | @Serializable 39 | data class InitialSync( 40 | val users: List, 41 | val chat: List, 42 | val games: Map 43 | ): LobbyEvent(), FromServer 44 | 45 | // TODO: event to announce new games 46 | // TODO: event to invite players to games 47 | 48 | companion object { 49 | fun install(builder: SerializersModuleBuilder) { 50 | builder.polymorphic { 51 | UpdateGame::class with UpdateGame.serializer() 52 | Message::class with Message.serializer() 53 | Join::class with Join.serializer() 54 | Leave::class with Leave.serializer() 55 | InitialSync::class with InitialSync.serializer() 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LoginRequest( 7 | val userId: String, 8 | val password: String? = null 9 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import kotlinx.serialization.Polymorphic 4 | import kotlinx.serialization.PolymorphicSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.modules.SerializersModuleBuilder 7 | 8 | @Polymorphic 9 | @Serializable(PolymorphicSerializer::class) 10 | sealed class LoginResponse { 11 | interface Failure 12 | 13 | @Serializable 14 | data class Success( 15 | val message: String 16 | ) : LoginResponse() 17 | 18 | @Serializable 19 | class UserIdRejected( 20 | val reason: String 21 | ) : LoginResponse(), Failure 22 | 23 | @Serializable 24 | object IncorrectPassword : LoginResponse(), Failure 25 | 26 | companion object { 27 | fun install(builder: SerializersModuleBuilder) { 28 | builder.polymorphic { 29 | Success::class with Success.serializer() 30 | UserIdRejected::class with UserIdRejected.serializer() 31 | IncorrectPassword::class with IncorrectPassword.serializer() 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/ServerStatus.kt: -------------------------------------------------------------------------------- 1 | package penta.network 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ServerStatus( 7 | val totalPlayers: Int 8 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/network/SessionEvent.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.serialization.modules.SerializersModuleBuilder 2 | import kotlinx.serialization.Serializable 3 | import kotlinx.serialization.Polymorphic 4 | import kotlinx.serialization.PolymorphicSerializer 5 | import penta.PlayerIds 6 | import penta.UserInfo 7 | import penta.network.GameEvent 8 | 9 | @Polymorphic 10 | @Serializable(PolymorphicSerializer::class) 11 | sealed class SessionEvent { 12 | @Serializable 13 | data class WrappedGameEvent( 14 | val event: GameEvent 15 | ) : SessionEvent() 16 | 17 | @Serializable 18 | data class PlayerJoin( 19 | val player: PlayerIds, // TODO: make enum 20 | val user: UserInfo 21 | ) : SessionEvent() 22 | 23 | @Serializable 24 | class PlayerLeave( 25 | val player: PlayerIds, // TODO: make enum 26 | val user: UserInfo 27 | ) : SessionEvent() 28 | 29 | @Serializable 30 | data class IllegalMove( 31 | val message: String, 32 | val move: WrappedGameEvent 33 | ) : SessionEvent() { 34 | 35 | } 36 | 37 | @Serializable 38 | data class Undo( 39 | val moves: List 40 | ) : SessionEvent() { 41 | 42 | } 43 | 44 | companion object { 45 | fun install(builder: SerializersModuleBuilder) { 46 | builder.polymorphic { 47 | WrappedGameEvent::class with WrappedGameEvent.serializer() 48 | PlayerJoin::class with PlayerJoin.serializer() 49 | PlayerLeave::class with PlayerLeave.serializer() 50 | IllegalMove::class with IllegalMove.serializer() 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/ColorUtil.kt: -------------------------------------------------------------------------------- 1 | //package penta.util 2 | // 3 | //import io.data2viz.color.Color 4 | //import io.data2viz.scale.ScalesChromatic 5 | //import mu.KotlinLogging 6 | // 7 | //private val logger = Logger(this::class.simpleName!!) 8 | //fun Color.interpolate(other: Color, steps: Int): List { 9 | // // TODO: use custom chromatic scales 10 | // val scale = ScalesChromatic.Continuous.linearHCL{ 11 | // domain = listOf(0.0, steps+1.0) 12 | // range = listOf(this@interpolate, other) 13 | // } 14 | // 15 | // val ticks = scale.ticks(steps+1).drop(1).dropLast(1) 16 | // logger.trace { "steps: $steps, ticks: $ticks, ${ticks.size}" } 17 | // return ticks.map { d -> 18 | // scale(d) 19 | // } 20 | //} 21 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/ExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import kotlinx.coroutines.CoroutineExceptionHandler 4 | import penta.IllegalMoveException 5 | import penta.PentaMove 6 | import kotlin.contracts.ExperimentalContracts 7 | import kotlin.contracts.contract 8 | 9 | val handler = CoroutineExceptionHandler { _, exception -> 10 | println("Caught $exception") 11 | } 12 | 13 | @UseExperimental(ExperimentalContracts::class) 14 | fun requireMove(value: Boolean, error: () -> PentaMove.IllegalMove): Nothing? { 15 | contract { 16 | returns() implies value 17 | } 18 | if (!value) { 19 | val illegalMove = error() 20 | throw IllegalMoveException(illegalMove) 21 | } 22 | return null 23 | } 24 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/ExhaustiveChecking.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | inline val T.exhaustive 4 | inline get() = this -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/Json.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonConfiguration 5 | import kotlinx.serialization.modules.SerializersModule 6 | import penta.network.GameEvent 7 | import penta.network.LobbyEvent 8 | import penta.network.LoginResponse 9 | 10 | val json = Json( 11 | JsonConfiguration( 12 | // unquotedPrint = false, 13 | allowStructuredMapKeys = false, //true, 14 | prettyPrint = false, 15 | classDiscriminator = "type" 16 | ), context = SerializersModule { 17 | GameEvent.install(this) 18 | SessionEvent.install(this) 19 | LoginResponse.install(this) 20 | LobbyEvent.install(this) 21 | }) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/ListExtension.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | inline fun MutableList.replaceLast(replace: E.() -> E) { 4 | set(lastIndex, replace(last())) 5 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/LoggerExtension.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import mu.KLogger 4 | 5 | suspend fun KLogger.suspendTrace(msg: suspend () -> Any?) { 6 | val msgValue = msg() 7 | trace { msgValue } 8 | } 9 | 10 | suspend fun KLogger.suspendDebug(msg: suspend () -> Any?) { 11 | val msgValue = msg() 12 | debug { msgValue } 13 | } 14 | 15 | suspend fun KLogger.suspendInfo(msg: suspend () -> Any?) { 16 | val msgValue = msg() 17 | info { msgValue } 18 | } 19 | 20 | suspend fun KLogger.suspendWarn(msg: suspend () -> Any?) { 21 | val msgValue = msg() 22 | warn { msgValue } 23 | } 24 | 25 | suspend fun KLogger.suspendError(msg: suspend () -> Any?) { 26 | val msgValue = msg() 27 | error { msgValue } 28 | } 29 | 30 | suspend fun KLogger.suspendTrace(e: Throwable?, msg: suspend () -> Any?) { 31 | val msgValue = msg() 32 | trace(e) { msgValue } 33 | } 34 | 35 | suspend fun KLogger.suspendDebug(e: Throwable?, msg: suspend () -> Any?) { 36 | val msgValue = msg() 37 | debug(e) { msgValue } 38 | } 39 | 40 | suspend fun KLogger.suspendInfo(e: Throwable?, msg: suspend () -> Any?) { 41 | val msgValue = msg() 42 | info(e) { msgValue } 43 | } 44 | 45 | suspend fun KLogger.suspendWarn(e: Throwable?, msg: suspend () -> Any?) { 46 | val msgValue = msg() 47 | warn(e) { msgValue } 48 | } 49 | 50 | suspend fun KLogger.suspendError(e: Throwable?, msg: suspend () -> Any?) { 51 | val msgValue = msg() 52 | error(e) { msgValue } 53 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/ObjectSerializer.kt: -------------------------------------------------------------------------------- 1 | //package penta.util 2 | // 3 | //import kotlinx.serialization.Decoder 4 | //import kotlinx.serialization.Encoder 5 | //import kotlinx.serialization.KSerializer 6 | //import kotlinx.serialization.SerialDescriptor 7 | // 8 | //@UseExperimental(kotlinx.serialization.InternalSerializationApi::class) 9 | //class ObjectSerializer(val obj: T) : KSerializer { 10 | // override val descriptor: SerialDescriptor = SerialDescriptor(obj::class.simpleName!!, kind = StructureKind.OBJECT) 11 | // override fun deserialize(decoder: Decoder): T { 12 | // return obj 13 | // } 14 | // 15 | // override fun serialize(encoder: Encoder, obj: T) { 16 | // val composite = encoder.beginStructure(descriptor) 17 | // composite.endStructure(descriptor) 18 | // } 19 | //} -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/penta/util/PointExtensions.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import io.data2viz.geom.Point 4 | import io.data2viz.math.deg 5 | import kotlin.math.sqrt 6 | 7 | val Point.length 8 | get() = sqrt((x * x) + (y * y)) 9 | 10 | val Point.unit: Point 11 | get() = this / length 12 | 13 | fun onCircle(r: Double, angle: Double): Point = Point( 14 | angle.deg.cos * r, 15 | angle.deg.sin * r 16 | ) 17 | 18 | fun Point.interpolate(otherPoint: Point, steps: Int = 1, skip: Double = 0.0): List { 19 | val v = Point( 20 | (otherPoint.x - x) / (steps + 1), 21 | (otherPoint.y - y) / (steps + 1) 22 | ).unit 23 | 24 | return (0 until steps).map { i -> 25 | this + (v * i) + v * skip 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %-55(%d{HH:mm:ss.SSS} [%thread] %-20(.\(%F:%L\))) %-5level - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/ClientJs.kt: -------------------------------------------------------------------------------- 1 | import com.soywiz.klogger.Logger 2 | import io.ktor.client.HttpClient 3 | import io.ktor.client.engine.js.Js 4 | import io.ktor.client.features.cookies.AcceptAllCookiesStorage 5 | import io.ktor.client.features.cookies.HttpCookies 6 | import io.ktor.client.features.json.JsonFeature 7 | import io.ktor.client.features.json.serializer.KotlinxSerializer 8 | import io.ktor.client.features.websocket.WebSockets 9 | import kotlinx.coroutines.Dispatchers 10 | import org.w3c.notifications.DENIED 11 | import org.w3c.notifications.GRANTED 12 | import org.w3c.notifications.Notification 13 | import org.w3c.notifications.NotificationOptions 14 | import org.w3c.notifications.NotificationPermission 15 | import penta.util.json 16 | 17 | private val logger = Logger("ClientKt") 18 | actual val client: HttpClient = HttpClient(Js).config { 19 | install(WebSockets) { 20 | 21 | } 22 | install(JsonFeature) { 23 | serializer = KotlinxSerializer(json) 24 | } 25 | install(HttpCookies) { 26 | // Will keep an in-memory map with all the cookies from previous requests. 27 | storage = AcceptAllCookiesStorage() 28 | } 29 | } 30 | 31 | actual val clientDispatcher = Dispatchers.Default 32 | actual fun showNotification(title: String, body: String) { 33 | Notification.requestPermission().then { 34 | when (it) { 35 | NotificationPermission.GRANTED -> { 36 | Notification( 37 | title, 38 | NotificationOptions( 39 | // badge = "badge", 40 | body = body 41 | ) 42 | ) 43 | } 44 | NotificationPermission.DENIED -> { 45 | logger.error { "notification denied" } 46 | } 47 | else -> { 48 | logger.error { "notification denied/else" } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/ConsoleExt.kt: -------------------------------------------------------------------------------- 1 | import kotlin.js.Console 2 | 3 | fun Console.debug(vararg o: Any?) { 4 | asDynamic().debug(o) 5 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/WrapperStore.kt: -------------------------------------------------------------------------------- 1 | import org.reduxkotlin.Store 2 | import redux.Reducer 3 | import redux.Store as RStore 4 | 5 | @Deprecated("nice try") 6 | class WrapperStore ( 7 | val backingStore: Store 8 | ) : RStore { 9 | override fun dispatch(action: A): Any { 10 | return backingStore.dispatch(action) 11 | } 12 | 13 | override fun getState(): S = backingStore.state 14 | 15 | override fun replaceReducer(nextReducer: Reducer) { 16 | backingStore.replaceReducer(nextReducer as (S, Any) -> S) 17 | } 18 | 19 | override fun subscribe(listener: () -> Unit): () -> Unit =backingStore.subscribe(listener) 20 | } -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/actions/Actions.kt: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import redux.RAction 4 | 5 | actual class Action actual constructor(override val action: A): RAction, ActionHolder -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/isUpperCase.kt: -------------------------------------------------------------------------------- 1 | actual val Char.isUpperCase: Boolean 2 | get() = (this == this.toUpperCase() && this != this.toLowerCase()) -------------------------------------------------------------------------------- /shared/src/jsMain/kotlin/launch.kt: -------------------------------------------------------------------------------- 1 | import kotlin.coroutines.Continuation 2 | import kotlin.coroutines.CoroutineContext 3 | import kotlin.coroutines.EmptyCoroutineContext 4 | import kotlin.coroutines.startCoroutine 5 | 6 | fun launch(block: suspend () -> Unit, errorHandler: (Throwable) -> Unit = {}) { 7 | block.startCoroutine(object : Continuation { 8 | override val context: CoroutineContext get() = EmptyCoroutineContext 9 | override fun resumeWith(result: Result) { 10 | result.recover(errorHandler) 11 | } 12 | }) 13 | } -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/actions/Actions.kt: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | actual data class Action actual constructor(override val action: A): ActionHolder -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/isUpperCase.kt: -------------------------------------------------------------------------------- 1 | actual val Char.isUpperCase: Boolean 2 | get() = isUpperCase() -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/penta/util/Middlewares.kt: -------------------------------------------------------------------------------- 1 | package penta.util 2 | 3 | import com.soywiz.klogger.Logger 4 | import org.reduxkotlin.middleware 5 | 6 | fun loggingMiddleware(logger: Logger) = middleware { store, next, action -> 7 | logger.info { 8 | "reduce action: $action" 9 | } 10 | //log here 11 | next(action) 12 | } 13 | 14 | fun test(c: Char) { 15 | c.isUpperCase() 16 | } -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.8 -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./gradlew frontend:clean frontend:browserWebpack \ 4 | && rsync -va frontend/build/distributions/* -e ssh shell.c-base.org:~/public_html/pentagame_dev/ 5 | 6 | -------------------------------------------------------------------------------- /versions.properties: -------------------------------------------------------------------------------- 1 | ## suppress inspection "SpellCheckingInspection" for whole file 2 | ## 3 | ## Dependencies and Plugin versions with their available updates 4 | ## Generated by $ ./gradlew refreshVersions 5 | ## Please, don't put extra comments in that file yet, keeping them is not supported yet. 6 | 7 | plugin.de.fayard.dependencies=0.5.1 8 | 9 | plugin.org.flywaydb.flyway=6.2.1 10 | 11 | version.ch.qos.logback..logback-classic=1.2.3 12 | ## # available=1.3.0-alpha5 13 | 14 | version.com.fasterxml.jackson.core..jackson-databind=2.10.2 15 | 16 | version.com.fasterxml.jackson.module..jackson-module-kotlin=2.10.2 17 | 18 | version.com.soywiz.korlibs.klogger..klogger=1.8.1 19 | 20 | version.com.soywiz.korlibs.klogger..klogger-js=1.8.1 21 | 22 | version.io.github.microutils..kotlin-logging=1.7.8 23 | 24 | version.io.github.microutils..kotlin-logging-common=1.7.8 25 | 26 | version.io.github.microutils..kotlin-logging-js=1.7.8 27 | 28 | version.kotlinx.coroutines=1.3.5 29 | 30 | version.kotlinx.serialization=0.20.0 31 | 32 | version.org.reduxkotlin..redux-kotlin=0.3.0 33 | 34 | version.org.reduxkotlin..redux-kotlin-reselect=0.2.10 35 | --------------------------------------------------------------------------------