{"id":"gmackie/playpath","name":"playpath","scope":"gmackie","platform":"roblox","description":"PlayPath learning platform SDK for Roblox - adaptive questions, mastery tracking, and event batching","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"aa28318f445f2103c6bd12e075283c6c10bedc743bd21094c61d805337a4a9ac","likes":0,"downloads":0,"install":"forest install gmackie/playpath","url":"https://forest.dev/p/roblox/gmackie/playpath","files":"https://api.forest.dev/ai/package/roblox/gmackie/playpath/files","readme":"# PlayPath SDK for Roblox\n\nServer-side Lua SDK for integrating [PlayPath](https://playpath.io) adaptive learning into Roblox games.\n\n## Installation\n\n### With Wally (recommended)\n\nAdd to your `wally.toml`:\n\n```toml\n[dependencies]\nPlayPath = \"gmackie/playpath@0.1.0\"\n```\n\nThen run:\n\n```bash\nwally install\n```\n\n### Manual Installation\n\nCopy `src/PlayPath.lua` into `ReplicatedStorage.PlayPath`.\n\n## Quick Start\n\n```lua\n-- ServerScriptService/PlayPathSetup.lua\nlocal Players = game:GetService(\"Players\")\nlocal PlayPath = require(game.ReplicatedStorage.PlayPath)\n\nPlayPath.init({\n    gameKeyId = \"your-game-key-id\",\n    apiKeySecret = \"your-api-secret\",\n})\n\nlocal sessions = {}\n\nPlayers.PlayerAdded:Connect(function(player)\n    PlayPath.createSession(player)\n        :andThen(function(session)\n            sessions[player] = session\n            \n            if not session.linked then\n                -- Show pairing UI with session.pairingCode\n            end\n            \n            return session:getNextQuestion()\n        end)\n        :andThen(function(response)\n            local question = response.question\n            -- Display question to player\n        end)\n        :catch(function(err)\n            warn(\"PlayPath error:\", err.message)\n        end)\nend)\n\nPlayers.PlayerRemoving:Connect(function(player)\n    local session = sessions[player]\n    if session then\n        session:endSession()\n        sessions[player] = nil\n    end\nend)\n```\n\n## API Reference\n\n### PlayPath.init(config)\n\nInitialize the SDK. Call once at server start.\n\n```lua\nPlayPath.init({\n    gameKeyId = \"your-key\",        -- Required\n    apiKeySecret = \"your-secret\",  -- Required\n    baseUrl = \"https://...\",       -- Optional, default: production\n    maxRetries = 3,                -- Optional\n    retryBackoffMs = 1000,         -- Optional\n    eventFlushInterval = 5,        -- Optional, seconds\n    eventFlushThreshold = 10,      -- Optional, events\n    logLevel = \"warn\",             -- Optional: \"none\"|\"error\"|\"warn\"|\"debug\"\n    mockMode = false,              -- Optional, for testing\n})\n```\n\n### PlayPath.createSession(player, options?)\n\nCreate a session for a player. Returns a Promise.\n\n```lua\nPlayPath.createSession(player, { launchToken = \"optional-lti-token\" })\n    :andThen(function(session)\n        print(session.sessionId)   -- string\n        print(session.linked)      -- boolean\n        print(session.pairingCode) -- string or nil\n        print(session.student)     -- {id, displayName} or nil\n        print(session.config)      -- {theme, focusSkills}\n    end)\n```\n\n### Session Methods\n\nAll methods return Promises except `trackEvent`.\n\n#### session:getNextQuestion(count?)\n\n```lua\nsession:getNextQuestion():andThen(function(response)\n    local question = response.question\n    print(question.id, question.prompt, question.choices)\nend)\n```\n\n#### session:submitAnswer(questionId, answer, responseTimeMs)\n\n```lua\nsession:submitAnswer(questionId, \"b\", 3500)\n    :andThen(function(result)\n        print(result.correct)      -- boolean\n        print(result.feedback)     -- string\n        print(result.masteryUpdates) -- array\n    end)\n```\n\n#### session:skipQuestion(questionId, reason)\n\n```lua\nsession:skipQuestion(questionId, \"too_hard\")\n```\n\n#### session:getHint(questionId, hintIndex?)\n\n```lua\nsession:getHint(questionId, 0):andThen(function(hint)\n    print(hint.hint)        -- string\n    print(hint.hintIndex)   -- number\n    print(hint.totalHints)  -- number\n    print(hint.isLastHint)  -- boolean\nend)\n```\n\n#### session:trackEvent(event)\n\nFire-and-forget event tracking. Events are batched automatically.\n\n```lua\nsession:trackEvent({\n    type = \"skill_demo\",\n    questionId = questionId,\n    correct = true,\n})\n```\n\n#### session:flush()\n\nManually flush pending events.\n\n```lua\nsession:flush():andThen(function(result)\n    print(result.accepted, result.rejected)\nend)\n```\n\n#### session:verifyPairingCode(code)\n\nLink an unlinked account.\n\n```lua\nsession:verifyPairingCode(\"ABC123\"):andThen(function(result)\n    if result.success then\n        print(\"Linked to:\", result.student.displayName)\n    end\nend)\n```\n\n#### session:endSession()\n\nEnd the session. Flushes pending events first.\n\n```lua\nsession:endSession():andThen(function()\n    print(\"Session ended\")\nend)\n```\n\n## Error Handling\n\nAll errors are structured:\n\n```lua\nsession:submitAnswer(...):catch(function(err)\n    print(err.code)       -- \"RATE_LIMITED\", \"UNAUTHORIZED\", etc.\n    print(err.message)    -- Human-readable message\n    print(err.statusCode) -- HTTP status or nil\n    print(err.retryable)  -- boolean\nend)\n```\n\n### Error Codes\n\n| Code | Retryable | Description |\n|------|-----------|-------------|\n| `VALIDATION_ERROR` | No | Invalid request |\n| `UNAUTHORIZED` | No | Invalid credentials |\n| `NOT_FOUND` | No | Resource not found |\n| `RATE_LIMITED` | Yes | Too many requests |\n| `INTERNAL_ERROR` | Yes | Server error |\n| `NETWORK_ERROR` | Yes | Network failure |\n| `SESSION_ENDED` | No | Session already ended |\n| `PLAYER_LEFT` | No | Player left game |\n\n## Mock Mode\n\nFor testing without API credentials:\n\n```lua\nPlayPath.init({\n    gameKeyId = \"test\",\n    apiKeySecret = \"test\",\n    mockMode = true,\n})\n```\n\n## Testing\n\nRun crypto self-tests in Studio:\n\n```lua\nlocal PlayPath = require(game.ReplicatedStorage.PlayPath)\nPlayPath._internal.runCryptoTests()\n```\n\n## License\n\nMIT\n","readmeTruncated":false}