Initial commit, adding original binaries, initial watch and companion apps which can talk

This commit is contained in:
ml
2026-06-01 15:53:11 +02:00
commit d7adb56197
63 changed files with 1787 additions and 0 deletions
@@ -0,0 +1,24 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("de.ladkau.schwertundmagieonpebblecompanionapp", appContext.packageName)
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SchwertUndMagieOnPebbleCompanionApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,37 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.graphics.Bitmap
import java.nio.ByteBuffer
class C64Engine {
// Native hooks representing underlying open-source emulator loops (e.g., Frodo Core)
external fun initEmulator(romPath: String): Boolean
external fun runFrame()
external fun getRawVideoBuffer(buffer: ByteBuffer) // Populates native 320x200 ARGB pixels
external fun injectNativeKey(keyCode: Int, isPressed: Boolean)
companion object {
init {
System.loadLibrary("frodoc64_core")
}
}
// Maps standard incoming string commands to traditional C64 matrix keycodes
fun handleWatchInput(command: String) {
val c64KeyCode = when (command) {
"1" -> 0x31 // C64 '1'
"2" -> 0x32 // C64 '2'
"3" -> 0x33 // C64 '3'
"Y" -> 0x59 // C64 'Y'
"N" -> 0x4E // C64 'N'
else -> return
}
// Simulates a clean hardware keypress cycle
Thread {
injectNativeKey(c64KeyCode, true)
Thread.sleep(100) // Hold down frame duration
injectNativeKey(c64KeyCode, false)
}.start()
}
}
@@ -0,0 +1,74 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import com.getpebble.android.kit.PebbleKit
import com.getpebble.android.kit.PebbleKit.PebbleDataReceiver
import com.getpebble.android.kit.util.PebbleDictionary
import java.util.UUID
class EmulatorService : Service() {
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
private val KEY_COMMAND = 1
private lateinit var emulatorEngine: C64Engine
private lateinit var watchStreamer: PebbleStreamer
private var isRunning = false
private var pebbleDataReceiver: PebbleDataReceiver? = null
override fun onCreate() {
super.onCreate()
emulatorEngine = C64Engine()
watchStreamer = PebbleStreamer(this)
// Boot standard disk path containing Schwert und Magie Part 1
val romPath = filesDir.absolutePath + "/schwert_und_magie_1.d64"
emulatorEngine.initEmulator(romPath)
setupPebbleInputReceiver()
startEmulationLoop()
}
private fun setupPebbleInputReceiver() {
pebbleDataReceiver = object : PebbleDataReceiver(PEBBLE_APP_UUID) {
override fun receiveData(context: Context?, transactionId: Int, data: PebbleDictionary) {
// Read ASCII string parsed by watch selections
val command = data.getString(KEY_COMMAND)
if (command != null) {
emulatorEngine.handleWatchInput(command)
}
}
}
PebbleKit.registerReceivedDataHandler(this, pebbleDataReceiver)
}
private fun startEmulationLoop() {
isRunning = true
Thread {
val TargetFrameTimeMs = 33 // Aiming roughly around standard video refresh increments
while (isRunning) {
val startTime = System.currentTimeMillis()
emulatorEngine.runFrame()
watchStreamer.processAndStreamFrame(emulatorEngine)
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < TargetFrameTimeMs) {
Thread.sleep(TargetFrameTimeMs - elapsed)
}
}
}.start()
}
override fun onDestroy() {
isRunning = false
super.onDestroy()
try {
unregisterReceiver(pebbleDataReceiver)
} catch (e: Exception) { /* Context safety cleanup */ }
}
override fun onBind(intent: Intent?): IBinder? = null
}
@@ -0,0 +1,100 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import fi.iki.elonen.NanoHTTPD
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private const val PORT = 8888
private const val TAG = "SuM"
class MainActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
private lateinit var tvTime: TextView
private lateinit var tvLog: TextView
private lateinit var scrollLog: ScrollView
private val mainHandler = Handler(Looper.getMainLooper())
private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private var server: CompanionServer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvStatus = findViewById(R.id.tv_status)
tvTime = findViewById(R.id.tv_time)
tvLog = findViewById(R.id.tv_log)
scrollLog = findViewById(R.id.scroll_log)
server = CompanionServer(PORT, timeFormat,
onTimeFetched = { time ->
mainHandler.post { tvTime.text = "Serving time: $time" }
},
onKey = { cmd ->
Log.d(TAG, "Key received: $cmd")
mainHandler.post { appendLog(cmd) }
}
)
try {
server?.start()
tvStatus.text = "Listening on localhost:$PORT"
Log.d(TAG, "HTTP server started on port $PORT")
} catch (e: Exception) {
tvStatus.text = "Server error: ${e.message}"
Log.e(TAG, "Failed to start server", e)
}
}
private fun appendLog(entry: String) {
val timestamp = timeFormat.format(Date())
tvLog.text = "[$timestamp] $entry\n${tvLog.text}"
scrollLog.post { scrollLog.scrollTo(0, 0) }
}
override fun onDestroy() {
super.onDestroy()
server?.stop()
}
}
class CompanionServer(
port: Int,
private val timeFormat: SimpleDateFormat,
private val onTimeFetched: (String) -> Unit,
private val onKey: (String) -> Unit
) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession): Response {
Log.d(TAG, "HTTP ${session.method} ${session.uri} params=${session.parameters}")
val response = when (session.uri) {
"/time" -> {
val time = timeFormat.format(Date())
onTimeFetched(time)
newFixedLengthResponse(
Response.Status.OK,
"application/json",
"""{"time":"$time"}"""
)
}
"/key" -> {
val cmd = session.parameters["cmd"]?.firstOrNull().orEmpty()
if (cmd.isNotEmpty()) onKey(cmd)
newFixedLengthResponse(Response.Status.OK, "text/plain", "ok")
}
else -> newFixedLengthResponse(
Response.Status.NOT_FOUND, "text/plain", "not found"
)
}
response.addHeader("Access-Control-Allow-Origin", "*")
return response
}
}
@@ -0,0 +1,76 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import android.content.Context
import android.graphics.Bitmap
import com.getpebble.android.kit.PebbleKit
import com.getpebble.android.kit.util.PebbleDictionary
import java.nio.ByteBuffer
import java.util.UUID
class PebbleStreamer(private val context: Context) {
// Matches the unique watch app configuration ID
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
private val KEY_FRAMEBUFFER = 0
private val c64Width = 320
private val c64Height = 200
private val targetWidth = 144
private val targetHeight = 168
private val rawVideoByteBuffer = ByteBuffer.allocateDirect(c64Width * c64Height * 4)
private val previousPackedFrame = ByteArray(3024) // Base baseline to identify delta frames
fun processAndStreamFrame(engine: C64Engine) {
rawVideoByteBuffer.clear()
engine.getRawVideoBuffer(rawVideoByteBuffer)
rawVideoByteBuffer.rewind()
// 1. Downsample raw output directly to 144x168 target matrix via threshold loop
val packedFrame = ByteArray(3024) // 144 * 168 / 8 bits
var byteIndex = 0
var bitCounter = 0
var currentByte = 0
// Calculate steps for nearest-neighbor scaling while stripping non-essential margins
val scaleX = c64Width.toFloat() / targetWidth.toFloat()
val scaleY = c64Height.toFloat() / targetHeight.toFloat()
for (y in 0 until targetHeight) {
val sourceY = (y * scaleY).toInt().coerceIn(0, c64Height - 1)
for (x in 0 until targetWidth) {
val sourceX = (x * scaleX).toInt().coerceIn(0, c64Width - 1)
// Read Pixel Integer (ARGB)
val pixelIdx = (sourceY * c64Width + sourceX) * 4
val r = rawVideoByteBuffer.get(pixelIdx + 1).toInt() and 0xFF
val g = rawVideoByteBuffer.get(pixelIdx + 2).toInt() and 0xFF
val b = rawVideoByteBuffer.get(pixelIdx + 3).toInt() and 0xFF
// Traditional relative luminance formula for 1-bit thresholding
val luminance = (0.299 * r + 0.587 * g + 0.114 * b)
val bit = if (luminance > 128) 1 else 0 // 1 = White background, 0 = Dark Text
// Pack bits securely into destination byte indices
currentByte = (currentByte shl 1) or bit
bitCounter++
if (bitCounter == 8) {
packedFrame[byteIndex++] = currentByte.toByte()
currentByte = 0
bitCounter = 0
}
}
}
// 2. Perform simple Dirty Frame Check to preserve BLE airwaves
if (packedFrame.contentEquals(previousPackedFrame)) {
return // Game screen is currently static; abort transfer
}
System.arraycopy(packedFrame, 0, previousPackedFrame, 0, packedFrame.size)
// 3. Dispatch the payload out to the connected Pebble Watch
val dict = PebbleDictionary()
dict.addBytes(KEY_FRAMEBUFFER, packedFrame)
PebbleKit.sendDataToPebble(context, PEBBLE_APP_UUID, dict)
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Watch: Not connected"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Sending: --:--:--"
android:textSize="24sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Keystrokes from watch:"
android:textSize="14sp"
android:textStyle="bold" />
<ScrollView
android:id="@+id/scroll_log"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:background="#F5F5F5">
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:padding="8dp"
android:textSize="13sp" />
</ScrollView>
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">SchwertUndMagieOnPebbleCompanionApp</string>
</resources>
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package de.ladkau.schwertundmagieonpebblecompanionapp
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}