Skip to content

Commit

Permalink
Merge pull request #48 from opatry/mosaic
Browse files Browse the repository at this point in the history
Mosaic
  • Loading branch information
opatry authored Jan 3, 2024
2 parents c59d820 + dc8db17 commit d516d9a
Show file tree
Hide file tree
Showing 15 changed files with 699 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .github/workflows/Build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Build
run: |
./syncComposeSharedSources.sh
./gradlew --no-daemon compileReleaseSources
./gradlew --no-daemon compileReleaseSources assembleDist
- name: Test
run: ./gradlew --no-daemon test
Expand All @@ -51,5 +51,6 @@ jobs:
-Pplaystore.keystore.password="$KEYSTORE_PASSWORD" \
-Pplaystore.keystore.key_password="$KEYSTORE_KEY_PASSWORD" \
:wordle-compose-desktop:assembleDist \
:wordle-compose-mosaic:assembleDist \
:wordle-compose-android:assembleRelease \
-x lint
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,29 @@ Congrats! You found the correct answer 🎉: HELLO

</details>

### Compose Mosaic mode

![](raw/wordle-mosaic.png)

In a compatible terminal, build the binary distribution and execute it:

```bash
$ ./gradlew wordle-compose-mosaic:installDist
$ ./wordle-compose-mosaic/build/install/wordle-compose-mosaic/bin/wordle-compose-mosaic
```

Or you can simply execute the convenience `runWordleMosaic.sh` script doing the same.

```bash
$ ./runWordleMosaic.sh
```

## Tech Stack

* [Kotlin](https://kotlinlang.org/)
* [Jetbrains Compose for Desktop](https://www.jetbrains.com/lp/compose/)
* [Jetpack Compose for Android](https://developer.android.com/jetpack/compose)
* [Mosaic Compose for Console UI](https://github.com/JakeWharton/mosaic)

## Development

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ plugins {
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.jetbrains.compose) apply false
alias(libs.plugins.mosaic) apply false
}

allprojects {
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ accompanist-insets = "com.google.accompanist:accompanist-insets:0.25.1"

gson = "com.google.code.gson:gson:2.10.1"

jline = "org.jline:jline:3.25.0"
turtle = "com.lordcodes.turtle:turtle:0.9.0"

junit4 = "junit:junit:4.13.2"

[bundles]
Expand All @@ -47,3 +50,4 @@ android-library = { id = "com.android.library", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" }
mosaic = { id = "com.jakewharton.mosaic", version = "0.9.1" }
Binary file added raw/wordle-mosaic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions runWordleMosaic.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -euo pipefail

origin=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) || exit

cd "$origin"

./gradlew wordle-compose-mosaic:installDist

echo ""

./wordle-compose-mosaic/build/install/wordle-compose-mosaic/bin/wordle-compose-mosaic
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ include("word-data")
include("wordle-ascii-cli")
include("wordle-compose-desktop")
include("wordle-compose-android")
include("wordle-compose-mosaic")
20 changes: 20 additions & 0 deletions wordle-compose-mosaic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
plugins {
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.mosaic)
application
}

dependencies {
implementation(libs.jline) {
because("need to handle terminal keyboard input")
}
implementation(libs.turtle) {
because("need to copy results to clipboard (using `pbcopy`, `xclip`, `clip` or equivalent)")
}
implementation(project(":word-data"))
implementation(project(":game-logic"))
}

application {
mainClass = "net.opatry.game.wordle.mosaic.WordleComposeMosaicKt"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2022 Olivier Patry
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package net.opatry.game.wordle

import com.lordcodes.turtle.shellRun
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

enum class OSName {
UNKNOWN,
WINDOWS,
MAC,
LINUX,
}

val OS: OSName
get() {
val os = System.getProperty("os.name").lowercase()
return when {
os.contains("mac") -> OSName.MAC
os.contains("win") -> OSName.WINDOWS
os.contains("nix") || os.contains("nux") || os.contains("aix") -> OSName.LINUX
else -> OSName.UNKNOWN
}
}

suspend fun String.copyToClipboard(): Boolean {
// TODO check for command availability and find fallbacks if possible
val copyCommand = when (OS) {
OSName.MAC -> "pbcopy"
OSName.WINDOWS -> "clip" // in WSL2 there is clipcopy
OSName.LINUX -> "xclip" // there is also xsel --clipboard --input
else -> null
}
return if (copyCommand != null) {
withContext(Dispatchers.IO) {
try {
// XXXcopy <<< "str"
shellRun("/bin/sh", listOf("-c", "$copyCommand <<< \"${this@copyToClipboard}\""))
true
} catch (e: Exception) {
false
}
}
} else {
false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2022 Olivier Patry
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package net.opatry.game.wordle.mosaic.component

import androidx.compose.runtime.Composable
import com.jakewharton.mosaic.ui.Column
import com.jakewharton.mosaic.ui.Row
import com.jakewharton.mosaic.ui.Text
import net.opatry.game.wordle.AnswerFlag

@Composable
fun Alphabet(alphabet: Map<Char, AnswerFlag>) {
Column {
val colCount = 9
val cellWidth = 5
Text("" + "".repeat(cellWidth * colCount) + "")
alphabet.keys.chunked(colCount).forEachIndexed { index, row ->
if (index > 0) {
Text("" + " ".repeat(colCount) + "")
}
Row {
Text("")
row.forEach { letter ->
// cell width is leading & trailing space + WordleCharCell compound of 3 characters
Text(" ")
WordleCharCell(letter, alphabet[letter]!!)
Text(" ")
}
// pad empty space for partial rows to align left border
repeat(colCount - row.size) {
Text(" ".repeat(cellWidth))
}
Text("")
}
}
Text("" + "".repeat(cellWidth * colCount) + "")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2022 Olivier Patry
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package net.opatry.game.wordle.mosaic.component

import androidx.compose.runtime.Composable
import com.jakewharton.mosaic.ui.Color
import com.jakewharton.mosaic.ui.Column
import com.jakewharton.mosaic.ui.Row
import com.jakewharton.mosaic.ui.Text
import com.jakewharton.mosaic.ui.TextStyle
import net.opatry.game.wordle.Answer
import net.opatry.game.wordle.AnswerFlag


@Composable
fun WordleGrid(grid: List<Answer>) {
Column {
// Box drawing: https://en.wikipedia.org/wiki/Box-drawing_character#Box_Drawing
// FIXME dividers length depends on max of row.letters
// good enough for now given that we know it's a 5x6 grid
Text("╭─────┬─────┬─────┬─────┬─────╮")
grid.forEachIndexed { rowIndex, row ->
if (rowIndex > 0) {
Text("├─────┼─────┼─────┼─────┼─────┤")
}
Row {
row.letters.forEachIndexed { cellIndex, char ->
if (cellIndex == 0)
Text("")
else
Text("")

WordleCharCell(char, row.flags[cellIndex])

if (cellIndex == row.letters.size - 1)
Text("")
}
}
}
Text("╰─────┴─────┴─────┴─────┴─────╯")
}
}

fun AnswerFlag.cellColors(): Pair<Color, Color> = when (this) {
AnswerFlag.NONE -> Color.Black to Color.White
AnswerFlag.PRESENT -> Color.Black to Color.Yellow
AnswerFlag.ABSENT -> Color.BrightWhite to Color.Black
AnswerFlag.CORRECT -> Color.BrightWhite to Color.Green
}

@Composable
fun WordleCharCell(char: Char, flag: AnswerFlag) {
val (foregroundColor, backgroundColor) =
if (flag == AnswerFlag.NONE && !char.isWhitespace())
Color.Black to Color.BrightWhite
else
flag.cellColors()

// TODO AnnotatedString " $char " https://github.com/JakeWharton/mosaic/issues/9
Column {
Row {
Text(
" $char ",
color = foregroundColor,
background = backgroundColor,
style = TextStyle.Bold
)
}
}
}
Loading

0 comments on commit d516d9a

Please sign in to comment.