test habit puzzle game in CI

This commit is contained in:
2022-05-09 16:31:25 -04:00
parent 063cacb26c
commit 075fd49d8e
13 changed files with 1 additions and 0 deletions

View File

@@ -0,0 +1 @@
export

View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"openfl.lime-vscode-extension"
]
}

View File

@@ -0,0 +1,21 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Build + Debug",
"type": "lime",
"request": "launch"
},
{
"name": "Debug",
"type": "lime",
"request": "launch",
"preLaunchTask": null
},
{
"name": "Macro",
"type": "haxe-eval",
"request": "launch"
}
]
}

View File

@@ -0,0 +1,13 @@
{
"search.exclude": {
"export/**/*.hx": true
},
"[haxe]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.sortImports": true
}
},
"haxe.enableExtendedIndentation": true
}

View File

@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "lime",
"command": "test",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<!-- _________________________ Application Settings _________________________ -->
<app title="FlxProject" file="FlxProject" main="Main" version="0.0.1" company="HaxeFlixel" />
<!--The flixel preloader is not accurate in Chrome. You can use it regularly if you embed the swf into a html file
or you can set the actual size of your file manually at "FlxPreloaderBase-onUpdate-bytesTotal"-->
<app preloader="flixel.system.FlxPreloader" />
<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
<set name="SWF_VERSION" value="11.8" />
<!-- ____________________________ Window Settings ___________________________ -->
<!--These window settings apply to all targets-->
<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />
<!--HTML5-specific-->
<window if="html5" resizable="false" />
<!--Desktop-specific-->
<window if="desktop" orientation="landscape" fullscreen="false" resizable="true" />
<!--Mobile-specific-->
<window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" />
<!-- _____________________________ Path Settings ____________________________ -->
<set name="BUILD_DIR" value="export" />
<classpath name="source" />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="flixel" />
<haxelib name="haxe-strings" />
<haxelib name="kiss" />
<haxelib name="kiss-tools" />
<haxelib name="datetime" />
<haxeflag name="--macro" value="kiss.Kiss.setup()" />
<!--In case you want to use the addons package-->
<!--<haxelib name="flixel-addons" />-->
<!--In case you want to use the ui package-->
<!--<haxelib name="flixel-ui" />-->
<!--In case you want to use nape with flixel-->
<!--<haxelib name="nape-haxe4" />-->
<!-- ______________________________ Haxedefines _____________________________ -->
<!-- Uncomment these and change the values to pre-compile character frequencies: -->
<!--<haxedef name="LEARN_FROM" value="path/to/text/folder"/>-->
<!--<haxedef name="LEARN_FROM_EXT" value="txt,markdown,etc"/>-->
<!--Enable the Flixel core recording system-->
<!--<haxedef name="FLX_RECORD" />-->
<!--Disable the right and middle mouse buttons-->
<!--<haxedef name="FLX_NO_MOUSE_ADVANCED" />-->
<!--Disable the native cursor API on Flash-->
<!--<haxedef name="FLX_NO_NATIVE_CURSOR" />-->
<!--Optimise inputs, be careful you will get null errors if you don't use conditionals in your game-->
<haxedef name="FLX_NO_MOUSE" if="mobile" />
<haxedef name="FLX_NO_KEYBOARD" if="mobile" />
<haxedef name="FLX_NO_TOUCH" if="desktop" />
<!--<haxedef name="FLX_NO_GAMEPAD" />-->
<!--Disable the Flixel core sound tray-->
<!--<haxedef name="FLX_NO_SOUND_TRAY" />-->
<!--Disable the Flixel sound management code-->
<!--<haxedef name="FLX_NO_SOUND_SYSTEM" />-->
<!--Disable the Flixel core focus lost screen-->
<!--<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />-->
<!--Disable the Flixel core debugger. Automatically gets set whenever you compile in release mode!-->
<haxedef name="FLX_NO_DEBUG" unless="debug" />
<!--Enable this for Nape release builds for a serious peformance improvement-->
<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
<!-- _________________________________ Custom _______________________________ -->
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
</project>

View File

@@ -0,0 +1,15 @@
{
"lineEnds": {
"leftCurly": "both",
"rightCurly": "both",
"objectLiteralCurly": {
"leftCurly": "after"
}
},
"sameLine": {
"ifElse": "next",
"doWhile": "next",
"tryBody": "next",
"tryCatch": "next"
}
}

View File

@@ -0,0 +1,35 @@
package;
import kiss.Prelude;
import kiss.List;
import kiss.Stream;
import sys.io.File;
import datetime.DateTime;
import datetime.DateTimeInterval;
enum EntryType {
Daily(daysOfWeek:Array<Int>, lastDayDone:String);
Interval(days:Int, lastDayDone:String);
// -1 represents the last day of the month, and so on
Monthly(daysOfMonth:Array<Int>, lastDayDone:String);
Bonus;
Todo;
}
typedef EntryLabel = {
label:String,
points:Int
};
typedef Entry = {
type: EntryType,
labels: Array<EntryLabel>,
};
typedef RewardFile = {
path: String,
startingPoints: Int
};
@:build(kiss.Kiss.build())
class HabitModel {}

View File

@@ -0,0 +1,204 @@
(prop :Array<Entry> dailyEntries [])
(prop :Array<Entry> monthlyEntries [])
(prop :Array<Entry> intervalEntries [])
(prop :Array<Entry> bonusEntries [])
(prop :Array<Entry> todoEntries [])
(prop :Array<RewardFile> rewardFiles [])
(defNew [&prop :String textFile]
(let [s (Stream.fromFile textFile)
&mut lastHeader ""]
// TODO could be whileLet
(loop
(case (s.takeLine)
((Some "DAILY")
(set lastHeader "DAILY"))
((Some "MONTHLY")
(set lastHeader "MONTHLY"))
((Some "INTERVAL")
(set lastHeader "INTERVAL"))
((Some "BONUS")
(set lastHeader "BONUS"))
((Some "TODO")
(set lastHeader "TODO"))
((Some "FILES")
(set lastHeader "FILES"))
((when (apply = (concat ["-"] (line.split ""))) (Some line))
(continue))
((Some "") (continue))
// Types won't unify with the next case, so this is its own:
((when (= lastHeader "FILES") (Some line))
(rewardFiles.push
(let [parts (line.split " ")
startingPoints (Std.parseInt (parts.pop))
path (parts.join " ")]
(objectWith path startingPoints))))
((Some line)
(.push
(case lastHeader
("DAILY" dailyEntries)
("MONTHLY" monthlyEntries)
("INTERVAL" intervalEntries)
("BONUS" bonusEntries)
("TODO" todoEntries)
(otherwise (throw "bad header")))
(object
type
(case lastHeader
("BONUS" Bonus)
("TODO" Todo)
("DAILY"
(case (line.split ":")
([noColon]
(Daily
// all days of week
(collect (range 7))
// never done before
""))
([::&mut preColon ...afterColon]
(set line (afterColon.join ":"))
(Daily
// Days of week specified by abbreviation:
(sort (filter
[
// disambiguate Th from T and Su from S:
(when (contains preColon "Th") {(set preColon (StringTools.replace preColon "Th" "")) 4})
(when (contains preColon "Su") {(set preColon (StringTools.replace preColon "Su" "")) 0})
(when (contains preColon "M") 1)
(when (contains preColon "T") 2)
(when (contains preColon "W") 3)
(when (contains preColon "F") 5)
(when (contains preColon "S") 6)
]))
// Last date completed after that:
(ifLet [[days date] (preColon.split " ")]
date
"")))
(otherwise (throw "bad line"))))
("MONTHLY"
(case (line.split ": ")
([::&mut preColon ...afterColon]
(set line (afterColon.join ": "))
(Monthly
// Days of month can be positive (1-31) or negative (-1 to -31)
(map (.split (first (preColon.split " ")) ",") Std.parseInt)
// Last date completed after that:
(ifLet [[::days ...date] (preColon.split " ")]
(date.join " ")
"")))
(otherwise (throw "bad line"))))
("INTERVAL"
(case (line.split ": ")
([::&mut preColon ...afterColon]
(set line (afterColon.join ": "))
(case (preColon.split " ")
([days]
(Interval (Std.parseInt days) ""))
([::days ...lastDayDone]
(Interval (Std.parseInt days) (lastDayDone.join " ")))
(otherwise (throw "bad interval habit: $line"))))
(otherwise (throw "bad interval habit: $line"))))
(otherwise (throw "bad header: $lastHeader")))
labels
(for l (line.split "/")
(object
label (StringTools.trim (StringTools.replace l "|" ""))
points (count (l.split "") ->c (= c "|")))))))
(otherwise (break))))))
(method :Int totalPoints []
(apply + (for l (flatten (for e (the Array<Entry> (concat dailyEntries monthlyEntries intervalEntries bonusEntries todoEntries)) e.labels)) l.points)))
(function :String stringify [:Entry e]
"$(case e.type
((Daily days lastDayDone)
(+
(.join (for day days
(case day
(0 "Su")
(1 "M")
(2 "T")
(3 "W")
(4 "Th")
(5 "F")
(6 "S")
(otherwise (throw "bad day")))) "")
" "
lastDayDone
": "))
((Monthly days lastDayDone)
"$(days.join ",") ${lastDayDone}: ")
((Interval days lastDayDone)
"$days ${lastDayDone}: ")
(otherwise ""))$(.join (for label e.labels
"${label.label} $(* "|" label.points)") "/")")
(function :String stringifyRewardFile [:RewardFile rewardFile]
"${rewardFile.path} ${rewardFile.startingPoints}")
(method :Void save []
(localVar &mut content "DAILY\n-----\n")
(+= content (.join (map dailyEntries stringify) "\n") "\n")
(+= content "\nMONTHLY\n--------\n")
(+= content (.join (map monthlyEntries stringify) "\n") "\n")
(+= content "\nINTERVAL\n--------\n")
(+= content (.join (map intervalEntries stringify) "\n") "\n")
(+= content "\nBONUS\n-----\n")
(+= content (.join (map bonusEntries stringify) "\n") "\n")
(+= content "\nTODO\n----\n")
(+= content (.join (map todoEntries stringify) "\n") "\n")
(+= content "\nFILES\n-----\n")
(+= content (.join (map rewardFiles stringifyRewardFile) "\n") "\n")
(File.saveContent textFile
content))
// With rotating entries, the active one is the first one with the lowest score:
(function :EntryLabel activeLabel [:Entry e]
(let [lowScore (apply min (for label e.labels label.points))]
(doFor label e.labels (when (= lowScore label.points) (return label)))
(throw "no active?!")))
(function todayString []
(let [d (Date.now)] "$(d.getDate)-$(+ 1 (d.getMonth))-$(d.getFullYear)"))
(function isActive [:Entry e]
(case e.type
((Daily days lastDayDone)
(and !(= lastDayDone (todayString)) (contains days (.getDay (Date.now)))))
((Monthly days lastDayDone)
// TODO logic
(let [&mut nextDay
(DateTime.fromDate (Date.now))
oneDayInterval (DateTimeInterval.create (DateTime.make null null 1) (DateTime.make null null 2))
dayToEndSearch
(if lastDayDone
(DateTime.fromString lastDayDone)
(let [now (DateTime.fromDate (Date.now))]
(until (= 1 (now.getDay)) #|now -= oneDayInterval;|#)
now))]
(until (and (= (nextDay.getDay) (dayToEndSearch.getDay)) (= (nextDay.getMonth) (dayToEndSearch.getMonth)) (= (nextDay.getYear) (dayToEndSearch.getYear)))
(let [daysInMonth (DateTime.daysInMonth (nextDay.getMonth) (nextDay.isLeapYear))
adjustedDays (for day days (% (+ daysInMonth day) daysInMonth))]
(when (contains adjustedDays (nextDay.getDay)) (return true)))
#|nextDay -= oneDayInterval|#)
(return false)))
((Interval days lastDayDone)
(or !lastDayDone (<= days #|(DateTime.fromDate(Date.now()) - DateTime.fromString(lastDayDone)).getTotalDays()|#)))
(Todo (= 0 .points (activeLabel e)))
(otherwise true)))
(method :Array<Entry> activeDailyEntries []
(filter dailyEntries isActive))
(method :Array<Entry> activeMonthlyEntries []
(filter monthlyEntries isActive))
(method :Array<Entry> activeIntervalEntries []
(filter intervalEntries isActive))
(method :Array<Entry> activeBonusEntries []
(filter bonusEntries isActive))
(method :Array<Entry> activeTodoEntries []
(filter todoEntries isActive))

View File

@@ -0,0 +1,21 @@
package;
import flash.display.BitmapData;
import haxe.io.Path;
import flixel.FlxG;
import flixel.FlxState;
import flixel.group.FlxGroup;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.text.FlxText;
import flixel.math.FlxRandom;
import flixel.math.FlxPoint;
import kiss.Prelude;
import kiss.List;
import kiss_tools.FlxKeyShortcutHandler;
import HabitModel;
import hx.strings.Strings;
import datetime.DateTime;
@:build(kiss.Kiss.build())
class HabitState extends FlxState {}

View File

@@ -0,0 +1,110 @@
(method &override :Void create [] (super.create))
(method &override :Void update [:Float elapsed]
(super.update elapsed)
// Hold left-click to hide the habit text and see the image clearly:
(when entryTexts (if FlxG.mouse.pressed (remove entryTexts) (add entryTexts)))
// Handle keyboard input:
(when shortcutHandler
(shortcutHandler.update)))
(prop &mut :FlxTypedGroup<FlxText> entryTexts null)
(prop &mut :FlxTypedGroup<FlxSprite> rewardBlockers null)
(prop &mut :FlxKeyShortcutHandler<Entry> shortcutHandler null)
(prop &mut :HabitModel model null)
(var PUZZLE_WIDTH 4)
(var PUZZLE_HEIGHT 4)
(var TOTAL_PIECES (* PUZZLE_WIDTH PUZZLE_HEIGHT))
(prop &mut :FlxSprite rewardSprite null)
(method setModel [m]
(set model m)
(set shortcutHandler (new FlxKeyShortcutHandler))
(let [p (m.totalPoints)
&mut i 0
&mut currentRewardFile null]
// Find, load, and add the current reward image as big as possible:
(while (> p .startingPoints (nth m.rewardFiles i))
(set currentRewardFile (nth m.rewardFiles i))
(if (>= ++i m.rewardFiles.length)
(break)))
(when rewardSprite
(remove rewardSprite))
(set rewardSprite (new FlxSprite 0 0 (BitmapData.fromFile (joinPath (Path.directory m.textFile) currentRewardFile.path))))
(rewardSprite.setGraphicSize FlxG.width 0)
(rewardSprite.updateHitbox)
(when (> rewardSprite.height FlxG.height)
(rewardSprite.setGraphicSize 0 FlxG.height))
(rewardSprite.updateHitbox)
(rewardSprite.screenCenter)
(add rewardSprite)
(when rewardBlockers
(remove rewardBlockers))
(set rewardBlockers (new FlxTypedGroup))
(add rewardBlockers)
(let [PIECE_WIDTH
(/ rewardSprite.width PUZZLE_WIDTH)
PIECE_HEIGHT
(/ rewardSprite.height PUZZLE_HEIGHT)
:Array<FlxPoint> blockerPoints []]
(doFor x (range PUZZLE_WIDTH)
(doFor y (range PUZZLE_HEIGHT)
(blockerPoints.push (new FlxPoint (+ rewardSprite.x (* x PIECE_WIDTH)) (+ rewardSprite.y (* y PIECE_HEIGHT))))))
// Cover it up with (TOTAL_PIECES - p) black squares placed randomly by choosing and removing from a zipped coordinate list
(let [r (new FlxRandom (Strings.hashCode currentRewardFile.path))]
(r.shuffle blockerPoints)
(doFor i (range (- (+ TOTAL_PIECES currentRewardFile.startingPoints) p))
(let [pos (nth blockerPoints i)
s (new FlxSprite pos.x pos.y)]
(s.makeGraphic (Math.ceil PIECE_WIDTH) (Math.ceil PIECE_HEIGHT) FlxColor.BLACK)
(rewardBlockers.add s))))))
(when entryTexts (remove entryTexts))
(set entryTexts (new FlxTypedGroup))
(set textY 0)
(set color FlxColor.ORANGE)
(map (m.activeDailyEntries) makeText)
(set color FlxColor.GREEN)
(map (m.activeMonthlyEntries) makeText)
(set color FlxColor.BLUE)
(map (m.activeIntervalEntries) makeText)
(set color FlxColor.WHITE)
(map (m.activeBonusEntries) makeText)
(set color FlxColor.YELLOW)
(map (m.activeTodoEntries) makeText)
(add entryTexts)
(doFor e (the Array<Entry> (concat m.dailyEntries m.monthlyEntries m.intervalEntries m.bonusEntries m.todoEntries))
(when (HabitModel.isActive e)
(let [label (HabitModel.activeLabel e)]
(shortcutHandler.registerItem label.label e))))
(set shortcutHandler.onBadKey ->:Void [_ _] {})
(set shortcutHandler.onSelectItem ->:Void [:Entry e]
(let [label (HabitModel.activeLabel e)]
(+= label.points 1)
(whenLet [(Daily days lastDayDone) e.type]
(set e.type (Daily days (HabitModel.todayString))))
(whenLet [(Monthly days lastDayDone) e.type]
(set e.type (Monthly days (.toString (DateTime.now)))))
(whenLet [(Interval days lastDayDone) e.type]
(set e.type (Interval days (.toString (DateTime.now)))))
(m.save)
(setModel m)
(shortcutHandler.start)))
(shortcutHandler.start))
(prop &mut textY 0)
(prop &mut :FlxColor color null)
(method makeText [:Entry e]
(let [label (HabitModel.activeLabel e)
text (new FlxText 0 textY 0 (+ label.label (* label.points "+")))]
(set text.color color)
(+= textY text.height)
(entryTexts.add text)))

View File

@@ -0,0 +1,17 @@
package;
import flixel.FlxG;
import flixel.FlxGame;
import openfl.display.Sprite;
class Main extends Sprite
{
public function new()
{
super();
addChild(new FlxGame(0, 0, HabitState, 1, 60, 60, true));
var t:HabitState = cast FlxG.state;
t.setModel(new HabitModel(Sys.args()[0]));
t.model.save();
}
}