Skip to content

Commit

Permalink
Merge pull request #31 from piknotech/threepoints
Browse files Browse the repository at this point in the history
Implement Three Points
  • Loading branch information
knothed committed Apr 1, 2021
2 parents 4615912 + d2c5851 commit da1b6a1
Show file tree
Hide file tree
Showing 45 changed files with 1,437 additions and 115 deletions.
2 changes: 1 addition & 1 deletion Arduino/SingleByteArduino/SingleByteArduino.ino
Expand Up @@ -5,7 +5,7 @@ void setup() {
}

void loop() {
if (Serial.available()) {
while (Serial.available()) {
Serial.read();
click();
}
Expand Down
1 change: 1 addition & 0 deletions Documentation/ImplementedGames.md
Expand Up @@ -6,6 +6,7 @@ The following is a full list of games which are playable by _gnimag_, or which a
| ------------- | ----------- | :----------------------------------------------------------- | ------------------------------------------------------------ |
| Mr Flap | Implemented | [App Store](https://apps.apple.com/at/app/mr-flap/id822206495) | [APK](https://apkpure.com/de/mr-flap/com.mrflap) |
| Flow Free | Implemented | [App Store](https://apps.apple.com/us/app/flow-free/id526641427) | [Play Store](https://play.google.com/store/apps/details?id=com.bigduckgames.flow) |
| Three Points | Implemented | [App Store](https://apps.apple.com/us/app/three-points/id901300583) ||
| identiti | Implemented | [App Store](https://apps.apple.com/de/app/identiti/id909914922) | [Play Store](https://play.google.com/store/apps/details?id=de.chipsapps.ca.identiti) |
| Freaking Math,<br>Freaking Math+ | Implemented<br>Implemented | [App Store](https://apps.apple.com/us/app/freaking-math/id846439108)<br>[App Store](https://apps.apple.com/us/app/freaking-math/id854825595) | [Play Store](https://play.google.com/store/apps/details?id=com.bangdev.freakingmath)<br>– |

Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions Documentation/WorldRecords.md
Expand Up @@ -18,3 +18,10 @@ identiti (iOS) | Freaking Math+ (iOS)
:-------------------------:|:-------------------------:
![](Resources/Highscores/identiti.png) | ![](Resources/Highscores/FreakingMath+.png)



## Three Points: 🥈

<p align="center">
<img src="Resources/Highscores/ThreePoints.png" width="350">
</p>
30 changes: 30 additions & 0 deletions gnimag/Modules/Base/Common/Sources/Extensions/Array+Sort.swift
@@ -0,0 +1,30 @@
//
// Created by David Knothe on 15.03.21.
// Copyright © 2019 - 2021 Piknotech. All rights reserved.
//

import Foundation

extension Array {
@_transparent
public mutating func sort<A>(by key: (Element) -> A, _ smaller: (A, A) -> Bool) {
sort { smaller(key($0), key($1)) }
}

@_transparent
public mutating func sort<A: Comparable>(by key: (Element) -> A) {
sort(by: key, <)
}

@_transparent
public func sorted<A>(by key: (Element) -> A, _ smaller: (A, A) -> Bool) -> [Element] {
var result = self
result.sort(by: key, smaller)
return result
}

@_transparent
public func sorted<A: Comparable>(by key: (Element) -> A) -> [Element] {
sorted(by: key, <)
}
}
2 changes: 1 addition & 1 deletion gnimag/Modules/Base/Common/Sources/SimpleClustering.swift
Expand Up @@ -104,7 +104,7 @@ public enum SimpleClustering {
}

// Sort clusters by size and return result
clusters.sort { $0.objects.count > $1.objects.count }
clusters.sort(by: \.objects.count, >)
return Result(clusters: clusters)
}
}
@@ -0,0 +1,33 @@
//
// Created by David Knothe on 13.02.20.
// Copyright © 2019 - 2021 Piknotech. All rights reserved.
//

/// Use OrphanageDetector to detect whether an object is orphaned, i.e. has not been updated recently.
/// This could happen either when such an object leaves the screen or when it was created erroneously.
public class OrphanageDetector {
/// The number of consecutive frames that have not been marked valid via `markAsValid`.
private var consecutiveNumberOfFramesWithoutUpdate = 0

/// When `newFrame` is called more than `maxFramesWithoutUpdate` times without a `markAsValid` in the meantime, this object gets marked as orphaned.
private let maxFramesWithoutUpdate: Int

public var isOrphaned: Bool {
consecutiveNumberOfFramesWithoutUpdate > maxFramesWithoutUpdate
}

/// Default initializer.
public init(maxFramesWithoutUpdate: Int) {
self.maxFramesWithoutUpdate = maxFramesWithoutUpdate
}

/// Call at the beginning of each frame. This increases the `consecutiveNumberOfFramesWithoutUpdate` counter.
public func newFrame() {
consecutiveNumberOfFramesWithoutUpdate += 1
}

/// Call when the object has been updated and is valid. This resets the `consecutiveNumberOfFramesWithoutUpdate` counter.
public func markAsValid() {
consecutiveNumberOfFramesWithoutUpdate = 0
}
}
Expand Up @@ -38,7 +38,6 @@ public class FramerateDetector {
public let tracker = LinearTracker(maxDataPoints: 50, tolerance: .absolute(0), maxDataPointsForLogging: 1000)
private var nextIndex = 0.0


/// Default initializer.
public init() {
}
Expand Down
9 changes: 5 additions & 4 deletions gnimag/Modules/Base/GameKit/Sources/GameQueue/GameQueue.swift
Expand Up @@ -15,7 +15,8 @@ public final class GameQueue {
public let timingStats: GameQueueTimingStats

/// More sophisticated detector regarding exact frame durations.
public let framerateDetector: FramerateDetector
/// Must explicitly be provided during initialization.
private let framerateDetector: FramerateDetector?

private let imageProvider: ImageProvider

Expand All @@ -33,11 +34,11 @@ public final class GameQueue {
private var nextFrameToAnalyze: Frame?

/// Default initializer.
public init(imageProvider: ImageProvider, synchronousFrameCallback: @escaping (Frame) -> Void) {
public init(imageProvider: ImageProvider, synchronousFrameCallback: @escaping (Frame) -> Void, framerateDetector: FramerateDetector? = nil) {
self.imageProvider = imageProvider
self.synchronousFrameCallback = synchronousFrameCallback
self.timingStats = GameQueueTimingStats(timeProvider: imageProvider.timeProvider)
self.framerateDetector = FramerateDetector()
self.framerateDetector = framerateDetector
}

/// Begin receiving images.
Expand Down Expand Up @@ -76,7 +77,7 @@ public final class GameQueue {
let incomingTime = imageProvider.timeProvider.currentTime

synchronized(self) {
framerateDetector.newFrame(time: frame.1)
framerateDetector?.newFrame(time: frame.1)
timingStats.newFrame(frame: frame, at: incomingTime)

// Note if the previous frame was dropped
Expand Down
Expand Up @@ -45,7 +45,7 @@ public struct RelativeTapSequence {
if isAlreadySorted {
self.taps = taps
} else {
self.taps = taps.sorted { $0.relativeTime < $1.relativeTime }
self.taps = taps.sorted(by: \.relativeTime)
}

self.unlockDuration = unlockDuration
Expand All @@ -59,7 +59,7 @@ public struct RelativeTapSequence {

extension RelativeTapSequence: CustomStringConvertible {
public var description: String {
let sortedTaps = taps.sorted { $0.relativeTime < $1.relativeTime }
let sortedTaps = taps.sorted(by: \.relativeTime)
return "RelativeTapSequence(taps: \(sortedTaps), unlockDuration: \(String(describing: unlockDuration)))"
}
}
Expand Down
Expand Up @@ -38,7 +38,7 @@ public final class TapDelayTracker {
/// Call when a tap has just been performed at the given time.
public func tapPerformed(_ tap: PerformedTap) {
performedTaps.append(tap)
performedTaps.sort { $0.scheduledFor < $1.scheduledFor }
performedTaps.sort(by: \.scheduledFor)
}

/// Call when a tap has just been detected at the given time.
Expand Down
Expand Up @@ -61,7 +61,7 @@ open class TapPredictorBase {
}

/// Override to create a predicted tap sequence for the current frame.
/// When returning nil, nothing happens (i.e. no taps and no lock), and predictionLogic will be called again next frame.
/// Returning nil is equivalent to returning an empty sequence.
open func predictionLogic() -> AbsoluteTapSequence? {
nil
}
Expand All @@ -73,6 +73,7 @@ open class TapPredictorBase {
/// Unschedule all scheduled taps and unlocking.
private func unschedule() {
scheduler.unscheduleAll()
tapSequence = nil
Timing.shared.cancelTasks(withObject: self) // Unschedule unlocking
}

Expand Down
Expand Up @@ -44,10 +44,7 @@ internal enum ConnectedComponents {
combine(components: &components, using: combineComponents)

// Sort left-to-right
components.sort { a, b in
a.region.xRange.center < b.region.xRange.center
}

components.sort(by: \.region.xRange.center)
return components
}

Expand Down
Expand Up @@ -8,7 +8,7 @@ import Foundation
import Image

/// EdgeDetector detects the edge of a shape which has a uniform color.
/// A starting point that is already at the edge is required.
/// A starting point inside the shape is required.
/// Where the image bounds are hit, the edge continues outside the bounds, unless inverse is true.
public enum EdgeDetector {
public enum DetectionLimit {
Expand Down
Expand Up @@ -28,7 +28,7 @@ public final class ScatterPlot {
yRange: SimpleRange<Double>? = nil,
rangeAugmentation: Double = 5%
) {
let dataPoints = dataPoints.sorted { $0.x < $1.x }
let dataPoints = dataPoints.sorted(by: \.x)

// Map sorted values to ChartDataEntries
let entries = dataPoints.map { ChartDataEntry(x: $0.x, y: $0.y) }
Expand Down
Expand Up @@ -18,6 +18,7 @@ struct SingleByteArduino: SomewhereTapper {
exit(withMessage: "Arduino not found.")
}
self.port = port
port.baudRate = 9600
port.open()
}

Expand Down
66 changes: 44 additions & 22 deletions gnimag/Modules/Executables/gnimag-cli/Sources/main.swift
Expand Up @@ -11,33 +11,55 @@ import YesNoMathGames
import FlowFree
import Geometry
import Tapping
import ThreePoints

Permissions.checkOnStartup()

// Arduino
let imageProvider = scrcpy.imageProvider.resizingImages(factor: 0.5)
let tapper = SingleByteArduino(portPath: "/dev/cu.usbmodem14101")

let mrflap = MrFlap(
imageProvider: imageProvider,
tapper: tapper,
debugParameters: DebugParameters(
location: NSHomeDirectory() +/ "Desktop/Debug.noSync",
occasions: [],
logEvery: 1000,
content: .all,
logLastCoupleFramesOnCrash: true
)
)
let arduino = SingleByteArduino(portPath: "/dev/cu.usbmodem14101")
let arguments = CommandLine.arguments
// let arguments = ["tp"]

Timing.shared.perform(after: 2) {
mrflap.play()
if arguments.contains("tap") {
Timing.shared.perform(after: 2) {
arduino.tap()
arduino.tap()
arduino.tap()
exit(0)
}
}

mrflap.crashed += {
print("CRASHED!")
else if arguments.contains("tp") {
Permissions.checkOnStartup()

let imageProvider = quickTime
let tapper = arduino
let threePoints = ThreePoints(imageProvider: imageProvider, tapper: tapper)

Timing.shared.perform(after: 2) {
threePoints.play()
}
}

PowerManager.disableScreenSleep()
else if arguments.contains("mrflap") {
Permissions.checkOnStartup()

let imageProvider = airServer // scrcpy.imageProvider.resizingImages(factor: 0.5)
let tapper = arduino

let mrflap = MrFlap(
imageProvider: imageProvider,
tapper: tapper,
debugParameters: DebugParameters(
location: NSHomeDirectory() +/ "Desktop/Debug.noSync",
occasions: [],
logEvery: 1000,
content: .all,
logLastCoupleFramesOnCrash: true
)
)

Timing.shared.perform(after: 2) {
mrflap.play()
}
}

PowerManager.disableScreenSleep()
RunLoop.main.run()
Expand Up @@ -58,7 +58,7 @@ class ImageAnalyzer {
}

let result = SimpleClustering.from(elements, maxDistance: 0.1)
let clusters = result.clusters.sorted { $0.size < $1.size }
let clusters = result.clusters.sorted(by: \.size)

// Check cluster validity: all clusters (except the background color) must have 2 elements
if (clusters.dropLast().any { $0.size != 2 }) {
Expand Down
1 change: 1 addition & 0 deletions gnimag/Modules/Games/GameTemplate/Sources/.keep
@@ -0,0 +1 @@

12 changes: 0 additions & 12 deletions gnimag/Modules/Games/GameTemplate/Sources/GameTemplate.swift

This file was deleted.

Expand Up @@ -43,6 +43,7 @@ class ImageAnalyzer {
return .failure(.error) & {debug.outcome = .error}
}
debug.coloring.result = coloring
lastColoring = coloring

// Decide whether player has crashed
if coloring.crashColor.matches(coloring.theme) {
Expand Down
Expand Up @@ -8,7 +8,7 @@ import GameKit

/// GameModelCollector accepts output from image analysis to create and update an up-to-date game model.
/// Before new results from image analysis are added, they are first checked for data integrity.
class GameModelCollector {
final class GameModelCollector {
let model: GameModel
let mode: GameMode

Expand Down Expand Up @@ -44,7 +44,7 @@ class GameModelCollector {

/// Use the AnalysisResult to update the game model.
/// Before actually updating the game model, the integrity of the result is checked.
/// Returns true if the game model has been updated; else, nothing of the data was integer.
/// Returns true if the game model has been updated; else, the data was not fully integer.
func accept(result: AnalysisResult, time: Double) -> Bool {
debugLogger.currentFrame.gameModelCollection.wasPerformed = true
defer { model.player.performDebugLogging() }
Expand Down

This file was deleted.

Expand Up @@ -11,7 +11,7 @@ import TestingTools
/// BarTracker bundles trackers for a single bar.
final class BarTracker {
/// Orphanage detector to see whether the bar tracker should be removed from the game model.
let orphanage = BarTrackerOrphanageDetector()
let orphanage = OrphanageDetector(maxFramesWithoutUpdate: 2)

/// The state the bar is currently in.
var state: BarTrackerState!
Expand Down Expand Up @@ -86,7 +86,7 @@ final class BarTracker {
/// Update the trackers with the values from the given bar.
/// Only call this AFTER a successful `integrityCheck`.
func update(with bar: Bar, at time: Double) {
orphanage.markBarAsValid()
orphanage.markAsValid()

angle.add(value: bar.angle, at: time)
width.add(value: bar.width)
Expand Down

0 comments on commit da1b6a1

Please sign in to comment.