diff --git a/dialect.yml b/dialect.yml
new file mode 100644
index 0000000..ba619a4
--- /dev/null
+++ b/dialect.yml
@@ -0,0 +1,51 @@
+version: 4
+
+output:
+ ver-mac/src/dialect.swift:
+ type: swift
+
+RootComponent:
+ type: component
+ oneliners:
+ - countText: print(c.countText)
+ - countText: RootVM.shared!.countText = c.countText
+ shoulds:
+ count:
+ 1. Upon hitting 10 the first time:
+ if:
+ c.recentField == F.didClickIncrement &&
+ c.count == 7
+ then:
+ c.count += 10
+ 2. Upon each button click:
+ if:
+ c.recentField == F.didClickIncrement
+ then:
+ c.count += 1
+ countText:
+ 1. Upon each count change
+ if:
+ c.recentField == F.count
+ then:
+ c.countText = "Count: '\(c.count)'"
+ 2. Upon launch
+ if:
+ c.recentField == F.didLaunch
+ then:
+ c.countText = "Press the button to count"
+ didLaunch:
+ 1. Only once during the first setup:
+ if:
+ c.recentField == F.didSetup &&
+ c.didLaunch == false
+ then:
+ c.didLaunch = true
+
+RootContext:
+ type: context
+ fields:
+ count: Int
+ countText: String
+ didClickIncrement: Bool
+ didLaunch: Bool
+ didSetup: Bool
diff --git a/game/ver-mac/Info.plist b/game/ver-mac/Info.plist
new file mode 100644
index 0000000..dc46284
--- /dev/null
+++ b/game/ver-mac/Info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleName
+ OGS Mahjong
+ CFBundleDisplayName
+ OGS Mahjong
+ CFBundleIdentifier
+ org.opengamestudio.Mahjong
+ CFBundleExecutable
+ ogs-mahjong
+ CFBundleIconFile
+ AppIcon
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 2.0.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 11.0
+ NSHighResolutionCapable
+
+
+
diff --git a/game/ver-mac/Package.swift b/game/ver-mac/Package.swift
new file mode 100644
index 0000000..e2b4221
--- /dev/null
+++ b/game/ver-mac/Package.swift
@@ -0,0 +1,15 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "OGSMahjong",
+ platforms: [
+ .macOS(.v13)
+ ],
+ targets: [
+ .executableTarget(
+ name: "ogs-mahjong",
+ path: "src"
+ )
+ ]
+)
diff --git a/game/ver-mac/Resources/AppIcon.icns b/game/ver-mac/Resources/AppIcon.icns
new file mode 100644
index 0000000..0fb5686
Binary files /dev/null and b/game/ver-mac/Resources/AppIcon.icns differ
diff --git a/game/ver-mac/src/AppDelegate.swift b/game/ver-mac/src/AppDelegate.swift
new file mode 100644
index 0000000..73db6af
--- /dev/null
+++ b/game/ver-mac/src/AppDelegate.swift
@@ -0,0 +1,21 @@
+import AppKit
+
+final class AppDelegate:
+ NSObject,
+ NSApplicationDelegate
+{
+ let root = RootComponent()
+ let rootVM = RootVM()
+
+ func applicationDidFinishLaunching(_ notification: Notification) {
+ // Bring the app to the foreground
+ NSApp.setActivationPolicy(.regular)
+ NSApp.activate(ignoringOtherApps: true)
+
+ root.setup()
+ }
+
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ true
+ }
+}
diff --git a/game/ver-mac/src/GameApp.swift b/game/ver-mac/src/GameApp.swift
new file mode 100644
index 0000000..65825ae
--- /dev/null
+++ b/game/ver-mac/src/GameApp.swift
@@ -0,0 +1,14 @@
+import AppKit
+import SwiftUI
+
+@main
+struct GameApp: App {
+ @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
+
+ var body: some Scene {
+ WindowGroup {
+ RootView(appDelegate.rootVM)
+ }
+ .defaultSize(width: 640, height: 480)
+ }
+}
diff --git a/game/ver-mac/src/dialect.swift b/game/ver-mac/src/dialect.swift
new file mode 100644
index 0000000..93af8f7
--- /dev/null
+++ b/game/ver-mac/src/dialect.swift
@@ -0,0 +1,276 @@
+
+// Reusable "none" constant
+public let DIALECT_CONTEXT_RECENT_FIELD_NONE = "none"
+
+// Base protocol for each component's state
+public protocol DialectContext {
+ var recentField: String { get set }
+
+ func field(_ name: String) -> T
+ func fieldAny(_ name: String) -> Any
+ mutating func setField(_ name: String, _ value: Any)
+}
+
+// Default implementation of `fieldAny` method for the protocol
+public extension DialectContext {
+ func fieldAny(_ name: String) -> Any {
+ return field(name)
+ }
+}
+
+// The engine of the dialect components
+public class DialectController {
+ var callbacks = [(DialectContext) -> Void]()
+ var context: DialectContext
+ var functions = [(DialectContext) -> DialectContext]()
+ var isProcessingQueue = false
+ var queue = [DialectContext]()
+
+ public init(_ context: DialectContext) {
+ self.context = context
+ }
+
+ func executeFunctions() {
+ let c = queue.removeFirst()
+ // Keep SSOT: Only allow single field change per should-function
+ context.recentField = c.recentField
+ context.setField(c.recentField, c.fieldAny(c.recentField))
+
+ for f in functions {
+ let ctx = f(context)
+ if ctx.recentField != DIALECT_CONTEXT_RECENT_FIELD_NONE {
+ queue.append(ctx)
+ }
+ }
+
+ reportContext()
+ }
+
+ func processQueue() {
+ // Prevent recursion.
+ if isProcessingQueue {
+ return
+ }
+
+ isProcessingQueue = true
+
+ while (queue.count > 0) {
+ executeFunctions()
+ }
+
+ isProcessingQueue = false
+ }
+
+ public func registerCallback(_ cb: @escaping (DialectContext) -> Void) {
+ callbacks.append(cb)
+ }
+
+ public func registerFieldCallback(
+ _ fieldName: String,
+ _ cb: @escaping (DialectContext) -> Void
+ ) {
+ callbacks.append({ c in
+ if c.recentField == fieldName {
+ cb(c)
+ }
+ })
+ }
+
+ public func registerFunction(_ f: @escaping (DialectContext) -> DialectContext) {
+ functions.append(f)
+ }
+
+ func reportContext() {
+ for cb in callbacks {
+ cb(context)
+ }
+ }
+
+ public func set(
+ _ fieldName: String,
+ _ value: Any
+ ) {
+ var c = context
+ c.setField(fieldName, value)
+ c.recentField = fieldName
+ queue.append(c)
+ processQueue()
+ }
+}
+
+// Register several oneliner callbacks to a controller
+func registerOneliners(
+ _ ctrl: DialectController,
+ _ items: [Any]
+) -> T? {
+ let halfCount = items.count / 2
+ for i in 0.. Void
+ ctrl.registerFieldCallback(field) { cc in
+ let c = cc as! T
+ callback(c)
+ }
+ }
+
+ // A hack for generics to operate
+ return nil
+}
+
+// Context field names for static type check
+struct F {
+ static let count = "count"
+ static let countText = "countText"
+ static let didClickIncrement = "didClickIncrement"
+ static let didLaunch = "didLaunch"
+ static let didSetup = "didSetup"
+
+}
+struct RootContext: DialectContext {
+ var count = Int()
+ var countText = String()
+ var didClickIncrement = Bool()
+ var didLaunch = Bool()
+ var didSetup = Bool()
+
+ var recentField = ""
+
+ func field(_ name: String) -> T {
+ if (name == "count") {
+ return count as! T
+ }
+ else if (name == "countText") {
+ return countText as! T
+ }
+ else if (name == "didClickIncrement") {
+ return didClickIncrement as! T
+ }
+ else if (name == "didLaunch") {
+ return didLaunch as! T
+ }
+ else if (name == "didSetup") {
+ return didSetup as! T
+ }
+
+ return "unknown-field-name" as! T
+ }
+
+ mutating func setField(
+ _ name: String,
+ _ value: Any
+ ) {
+ if (name == "count") {
+ count = value as! Int
+ }
+ else if (name == "countText") {
+ countText = value as! String
+ }
+ else if (name == "didClickIncrement") {
+ didClickIncrement = value as! Bool
+ }
+ else if (name == "didLaunch") {
+ didLaunch = value as! Bool
+ }
+ else if (name == "didSetup") {
+ didSetup = value as! Bool
+ }
+
+ }
+}
+
+func rootSet(
+ _ key: String,
+ _ value: Any
+) {
+ RootComponent.singleton!.ctrl.set(key, value)
+}
+
+func rootShouldResetCount(_ c: RootContext) -> RootContext {
+ var c = c
+
+ /* 1. Upon hitting 10 the first time */
+ if
+ c.recentField == F.didClickIncrement &&
+ c.count == 7
+ {
+ c.count += 10
+ c.recentField = F.count
+ return c
+ }
+
+ /* 2. Upon each button click */
+ if
+ c.recentField == F.didClickIncrement
+ {
+ c.count += 1
+ c.recentField = F.count
+ return c
+ }
+
+
+ c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE
+ return c
+}
+
+func rootShouldResetCountText(_ c: RootContext) -> RootContext {
+ var c = c
+
+ /* 1. Upon each count chang */
+ if
+ c.recentField == F.count
+ {
+ c.countText = "Count: '\(c.count)'"
+ c.recentField = F.countText
+ return c
+ }
+
+ /* 2. Upon launc */
+ if
+ c.recentField == F.didLaunch
+ {
+ c.countText = "Press the button to count"
+ c.recentField = F.countText
+ return c
+ }
+
+
+ c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE
+ return c
+}
+
+func rootShouldResetDidLaunch(_ c: RootContext) -> RootContext {
+ var c = c
+
+ /* 1. Only once during the first setup */
+ if
+ c.recentField == F.didSetup &&
+ c.didLaunch == false
+ {
+ c.didLaunch = true
+ c.recentField = F.didLaunch
+ return c
+ }
+
+
+ c.recentField = DIALECT_CONTEXT_RECENT_FIELD_NONE
+ return c
+}
+
+func rootRegisterShoulds(_ ctrl: DialectController) {
+ [
+ rootShouldResetCount,
+ rootShouldResetCountText,
+ rootShouldResetDidLaunch,
+
+ ].forEach { f in
+ ctrl.registerFunction { c in f(c as! RootContext) }
+ }
+}
+
+func rootRegisterEffects(_ ctrl: DialectController) {
+ let _: RootContext? = registerOneliners(ctrl, [
+ F.countText, { (c: RootContext) in print(c.countText) },
+ F.countText, { (c: RootContext) in RootVM.shared!.countText = c.countText },
+
+ ])
+}