From c74a5c3ca73235ff523db1ec700b0f26092e78d4 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 11:49:38 -0500 Subject: [PATCH 01/40] complete basic project setup --- .github/workflows/ci.yml | 33 ++++ Pipfile | 14 ++ Pipfile.lock | 391 +++++++++++++++++++++++++++++++++++++++ study_pet/__init__.py | 2 + study_pet/tracker.py | 2 + tests/test_tracker.py | 15 ++ 6 files changed, 457 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 study_pet/__init__.py create mode 100644 study_pet/tracker.py create mode 100644 tests/test_tracker.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d36e7eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + test: + strategy: + matrix: + python-version: ["3.11", "3.12"] + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies (pipenv) + run: | + python -m pip install --upgrade pip + pip install pipenv + pipenv install --dev + + - name: Run tests + run: | + pipenv run pytest --maxfail=1 --disable-warnings -q diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..bd9ce6c --- /dev/null +++ b/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +pytest = "*" +build = "*" +twine = "*" + +[requires] +python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..03e8dce --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,391 @@ +{ + "_meta": { + "hash": { + "sha256": "e0932273ceead75e720b415b9885af7f9edc69cc362c85ffb8d686cfdb86b623" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "build": { + "hashes": [ + "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", + "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.3.0" + }, + "certifi": { + "hashes": [ + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "docutils": { + "hashes": [ + "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", + "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.2" + }, + "id": { + "hashes": [ + "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", + "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "jaraco.classes": { + "hashes": [ + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.1" + }, + "jaraco.functools": { + "hashes": [ + "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", + "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294" + ], + "markers": "python_version >= '3.9'", + "version": "==4.3.0" + }, + "keyring": { + "hashes": [ + "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" + ], + "markers": "python_version >= '3.9'", + "version": "==25.6.0" + }, + "markdown-it-py": { + "hashes": [ + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" + ], + "markers": "python_version >= '3.10'", + "version": "==4.0.0" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", + "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" + ], + "markers": "python_version >= '3.9'", + "version": "==10.8.0" + }, + "nh3": { + "hashes": [ + "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", + "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", + "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", + "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", + "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", + "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", + "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", + "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", + "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", + "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", + "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", + "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", + "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", + "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", + "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", + "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", + "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", + "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", + "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", + "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", + "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", + "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", + "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", + "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", + "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", + "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a" + ], + "markers": "python_version >= '3.8'", + "version": "==0.3.2" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pyproject-hooks": { + "hashes": [ + "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", + "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + ], + "markers": "python_version >= '3.6'", + "version": "==0.2.3" + }, + "readme-renderer": { + "hashes": [ + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" + ], + "markers": "python_version >= '3.9'", + "version": "==44.0" + }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", + "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==14.2.0" + }, + "twine": { + "hashes": [ + "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", + "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.2.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + ], + "markers": "python_version >= '3.9'", + "version": "==2.5.0" + } + } +} diff --git a/study_pet/__init__.py b/study_pet/__init__.py new file mode 100644 index 0000000..0e9a102 --- /dev/null +++ b/study_pet/__init__.py @@ -0,0 +1,2 @@ +def hello(): + return "🐾 Hello from StudyPet!" diff --git a/study_pet/tracker.py b/study_pet/tracker.py new file mode 100644 index 0000000..f3049d4 --- /dev/null +++ b/study_pet/tracker.py @@ -0,0 +1,2 @@ +def track(): + return "Tracking session started!" diff --git a/tests/test_tracker.py b/tests/test_tracker.py new file mode 100644 index 0000000..6c0f681 --- /dev/null +++ b/tests/test_tracker.py @@ -0,0 +1,15 @@ +import sys, os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + + +from study_pet import hello +from study_pet.tracker import track + + +def test_hello(): + assert "Hello" in hello() + + +def test_track(): + assert "Tracking" in track() From 4651edb43ce59fc3871865bf220afa2b6567b71f Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 11:56:43 -0500 Subject: [PATCH 02/40] update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d36e7eb..5dd8b97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: ["main"] + branches: ["main", "pipfile-experiment"] pull_request: - branches: ["main"] + branches: ["main", "pipfile-experiment"] jobs: test: From aa2c0190a2ba96ad4c8df7c4e4151337590b7a41 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 12:01:22 -0500 Subject: [PATCH 03/40] Fix CI Python version mismatch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dd8b97..151f60e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] runs-on: ubuntu-latest steps: From 6615e11a83303d60d13494633ac7c5a26f76ea69 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 12:04:32 -0500 Subject: [PATCH 04/40] Fix CI Python version mismatch #2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 151f60e..afff7eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.12", "3.13"] runs-on: ubuntu-latest steps: From 526ca9d8037078ee2a5a7120f0688eb5e33b4985 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 12:09:42 -0500 Subject: [PATCH 05/40] Fix CI Python version mismatch #3 --- .github/workflows/Pipfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Pipfile b/.github/workflows/Pipfile index f5d05e5..e71e47d 100644 --- a/.github/workflows/Pipfile +++ b/.github/workflows/Pipfile @@ -12,4 +12,5 @@ python-dateutil = "*" [dev-packages] [requires] -python_version = "3" +python_version = ">=3.10" + From 0232030d27251c4fda3f610245c21f5592c29a31 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 12:14:38 -0500 Subject: [PATCH 06/40] Fix CI Python version mismatch #4 --- .github/workflows/Pipfile | 3 +-- .github/workflows/ci.yml | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Pipfile b/.github/workflows/Pipfile index e71e47d..f5d05e5 100644 --- a/.github/workflows/Pipfile +++ b/.github/workflows/Pipfile @@ -12,5 +12,4 @@ python-dateutil = "*" [dev-packages] [requires] -python_version = ">=3.10" - +python_version = "3" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afff7eb..877e542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - python-version: ["3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13"] runs-on: ubuntu-latest steps: @@ -22,11 +22,11 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies (pipenv) + - name: Install dependencies run: | python -m pip install --upgrade pip pip install pipenv - pipenv install --dev + pipenv install --dev --skip-lock --python $(which python) - name: Run tests run: | From 4d734091528ed5341a59219794fc0399a77cf830 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 15:30:18 -0500 Subject: [PATCH 07/40] added basic datamanagement function --- study_pet/data_manager.py | 48 +++++++++++++++++++++++++++++ study_pet/tracker.py | 65 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 study_pet/data_manager.py diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py new file mode 100644 index 0000000..e21da39 --- /dev/null +++ b/study_pet/data_manager.py @@ -0,0 +1,48 @@ +import json +import os + +DATA_DIR = os.path.expanduser("~/.study_pet") +DATA_PATH = os.path.join(DATA_DIR, "data.json") + +default_state = { + "name": "PomPom", + "level": 1, + "experience": 0, + "total_study_time": 0.0, + "last_session_start": None, +} + + +def ensure_data_dir(): + if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR, exist_ok=True) + + +def load_state(): + """ """ + ensure_data_dir() + if not os.path.exists(DATA_PATH): + save_state(default_state) + try: + with open(DATA_PATH, "r") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + save_state(default_state) + return default_state.copy() + + +def save_state(state: dict): + ensure_data_dir() + with open(DATA_PATH, "w") as f: + json.dump(state, f, indent=4) + + +def reset_state(): + save_state(default_state.copy()) + + +if __name__ == "__main__": + print("📁 Checking data directory:", DATA_DIR) + reset_state() + print("✅ Pet data initialized at:", DATA_PATH) + print("📂 Current state:", load_state()) diff --git a/study_pet/tracker.py b/study_pet/tracker.py index f3049d4..df987f3 100644 --- a/study_pet/tracker.py +++ b/study_pet/tracker.py @@ -1,2 +1,63 @@ -def track(): - return "Tracking session started!" +""" + +Records when the user starts and ends a session, +and updates total study time in the global JSON file. +""" + +import time +from .data_manager import load_state, save_state + + +def start_session(): + """ + Marks the beginning of a study session. + Stores the current timestamp in the state file. + """ + state = load_state() + if state.get("last_session_start") is not None: + print("Warning: A session is already running!") + return + + state["last_session_start"] = time.time() + save_state(state) + print("Study session started!") + + +def end_session(): + """ + Ends the study session, calculates duration, + and updates total study time in hours. + """ + state = load_state() + start_time = state.get("last_session_start") + + if start_time is None: + print("No active study session found.") + return + + end_time = time.time() + duration = (end_time - start_time) / 3600 # convert seconds to hours + state["total_study_time"] += duration + state["last_session_start"] = None + + save_state(state) + print(f"Great Job! You studied for {duration:.2f} hours.") + print(f"Total study time: {state['total_study_time']:.2f} hours.") + + +def get_total_time(): + """ + Returns the total accumulated study time (in hours). + """ + state = load_state() + return state.get("total_study_time", 0.0) + + +# temp testing code +if __name__ == "__main__": + print("=== Study Session Tracker Test ===") + start_session() + print("Simulating study for 3 seconds...") + time.sleep(10) # Simulate a study session of 10 seconds + end_session() + print("Total study time:", get_total_time(), "hours") From 609693039b09e88d89654b7f7f8603841b011ddf Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 15:48:52 -0500 Subject: [PATCH 08/40] update pytest file for dataManagement file --- tests/test_data_manager.py | 41 +++++++++++++++++++++++++++++++++++ tests/test_tracker.py | 44 ++++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 tests/test_data_manager.py diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py new file mode 100644 index 0000000..26c8fbf --- /dev/null +++ b/tests/test_data_manager.py @@ -0,0 +1,41 @@ +import os +import sys +import json +import pytest + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from study_pet import data_manager as dm + + +@pytest.fixture(autouse=True) +def cleanup_data_file(): + if os.path.exists(dm.DATA_PATH): + os.remove(dm.DATA_PATH) + yield + if os.path.exists(dm.DATA_PATH): + os.remove(dm.DATA_PATH) + + +def test_load_state_creates_default_file(): + state = dm.load_state() + assert os.path.exists(dm.DATA_PATH) + assert "name" in state + assert state["level"] == 1 + + +def test_save_state_writes_to_file(): + test_state = {"name": "Testy", "level": 5, "experience": 99} + dm.save_state(test_state) + with open(dm.DATA_PATH, "r") as f: + data = json.load(f) + assert data["name"] == "Testy" + assert data["level"] == 5 + + +def test_reset_state_resets_to_default(): + dm.save_state({"name": "WrongPet", "level": 10}) + dm.reset_state() + state = dm.load_state() + assert state["name"] == "PomPom" + assert state["level"] == 1 diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 6c0f681..7e99a8f 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -1,15 +1,47 @@ import sys, os +import time +import pytest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from study_pet import hello -from study_pet.tracker import track +from study_pet.tracker import start_session, end_session, get_total_time +from study_pet.data_manager import load_state, reset_state, save_state -def test_hello(): - assert "Hello" in hello() +@pytest.fixture(autouse=True) +def clean_data_file(): + reset_state() + yield + reset_state() -def test_track(): - assert "Tracking" in track() +def test_start_session_creates_timestamp(): + start_session() + state = load_state() + assert state["last_session_start"] is not None + + +def test_end_session_updates_total_time(): + start_session() + time.sleep(0.5) + end_session() + state = load_state() + assert state["total_study_time"] > 0 + assert state["last_session_start"] is None + + +def test_get_total_time_matches_state(): + state = load_state() + state["total_study_time"] = 12.34 + save_state(state) + total_time = get_total_time() + assert abs(total_time - 12.34) < 0.001 + + +def test_end_session_without_start_does_not_crash(): + try: + end_session() + assert True + except Exception as e: + pytest.fail(f"end_session() raised an unexpected exception: {e}") From 8b160c8830a63eb13ca2dd8f57bc73a3dc01c6cf Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 19:21:25 -0500 Subject: [PATCH 09/40] added core logic of leveling and store time data --- study_pet/data_manager.py | 5 +- study_pet/pet/__init__.py | 3 ++ study_pet/pet/core.py | 97 +++++++++++++++++++++++++++++++++++++++ study_pet/tracker.py | 84 +++++++++++++++++++++++---------- tests/test_core.py | 42 +++++++++++++++++ tests/test_tracker.py | 10 +++- 6 files changed, 214 insertions(+), 27 deletions(-) create mode 100644 study_pet/pet/__init__.py create mode 100644 study_pet/pet/core.py create mode 100644 tests/test_core.py diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py index e21da39..af4f11b 100644 --- a/study_pet/data_manager.py +++ b/study_pet/data_manager.py @@ -7,9 +7,12 @@ default_state = { "name": "PomPom", "level": 1, - "experience": 0, + "experience": 0.0, "total_study_time": 0.0, "last_session_start": None, + "last_study_date": None, + "mood": 100, + "streak_days": 0, } diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py new file mode 100644 index 0000000..4fd80df --- /dev/null +++ b/study_pet/pet/__init__.py @@ -0,0 +1,3 @@ +from .core import update_pet, get_status + +__all__ = ["update_pet", "get_status"] diff --git a/study_pet/pet/core.py b/study_pet/pet/core.py new file mode 100644 index 0000000..d783f25 --- /dev/null +++ b/study_pet/pet/core.py @@ -0,0 +1,97 @@ +""" +study_pet/pet/core.py +------------------------------------------- +Core logic for StudyPet: +Handles leveling, experience, and status updates +based on total study time. + +Design principles: +- update_pet(): true update (writes to persistent JSON) +- get_status(): live preview (uses current session time if any, no write) +""" + +from ..data_manager import load_state, save_state +from datetime import datetime +import time + + +def _calculate_level_exp(total_hours: float): + """ + Convert total study hours to (level, exp). + + Level = floor(total_hours / 5) + 1 + EXP = (total_hours % 5) * 20 + (Every 5 hours = +1 level, 100 EXP = level up) + """ + level = int(total_hours // 5) + 1 + exp = (total_hours % 5) * 20 + return level, exp + + +# called when session ends +def update_pet(): + """ + Performs a *true* update of the pet’s level and experience, + writing the results to persistent storage. + """ + state = load_state() + total_time = state.get("total_study_time", 0.0) + prev_level = state.get("level", 1) + + new_level, new_exp = _calculate_level_exp(total_time) + + if new_level > prev_level: + print(f" {state['name']} leveled up! {prev_level} → {new_level}") + + # Update state values + state["level"] = new_level + state["experience"] = new_exp + state["last_study_date"] = datetime.now().strftime("%Y-%m-%d") + + save_state(state) + return state + + +# shows real time data +def get_status(): + """ + Does NOT modify the JSON file. + """ + state = load_state() + total_time = state.get("total_study_time", 0.0) + last_start = state.get("last_session_start", None) + + # If currently studying, include elapsed time + if last_start is not None: + elapsed = (time.time() - last_start) / 3600 + total_time += elapsed + else: + elapsed = 0.0 + + # compute simulate level/exp + level, exp = _calculate_level_exp(total_time) + + name = state.get("name", "Unnamed") + mood = state.get("mood", 100) + streak = state.get("streak_days", 0) + last_study = state.get("last_study_date", "N/A") + + studying = "Studying now" if last_start else " Idle" + + status = ( + f"\n Pet Status (Live Preview)\n" + f"Name: {name}\n" + f"Level: {level} ({exp:.0f} EXP)\n" + f"Total Study Time: {total_time:.2f} hrs (+{elapsed:.2f}h current)\n" + f"Last Study: {last_study}\n" + f"Mood: {mood}/100 Streak: {streak} days\n" + f"Session: {studying}" + ) + + return status + + +# Manual test +if __name__ == "__main__": + print("🔁 Checking pet status preview...") + print(get_status()) diff --git a/study_pet/tracker.py b/study_pet/tracker.py index df987f3..9f81112 100644 --- a/study_pet/tracker.py +++ b/study_pet/tracker.py @@ -1,63 +1,97 @@ """ +Tracks study sessions for StudyPet. -Records when the user starts and ends a session, -and updates total study time in the global JSON file. +Responsible for: +- Starting and ending sessions +- Updating total study time +- Triggering pet updates (level, exp) """ +import atexit +import signal import time +from datetime import datetime from .data_manager import load_state, save_state +from .pet.core import update_pet def start_session(): """ - Marks the beginning of a study session. - Stores the current timestamp in the state file. + Begins a study session. + If a session is already active, it will not start a new one. """ state = load_state() - if state.get("last_session_start") is not None: - print("Warning: A session is already running!") + + if state.get("last_session_start"): + print("A study session is already active.") return state["last_session_start"] = time.time() save_state(state) - print("Study session started!") + print(f"📘 Study session started at {datetime.now().strftime('%H:%M:%S')}") def end_session(): """ - Ends the study session, calculates duration, - and updates total study time in hours. + Ends a study session and updates total study time. + Also triggers a pet level/exp update. """ state = load_state() - start_time = state.get("last_session_start") + start_time = state.get("last_session_start", None) - if start_time is None: - print("No active study session found.") + if not start_time: + print("⚠️ No active study session found.") return + # Calculate elapsed time in hours end_time = time.time() - duration = (end_time - start_time) / 3600 # convert seconds to hours - state["total_study_time"] += duration + elapsed_hours = (end_time - start_time) / 3600 + + # Update totals + state["total_study_time"] += elapsed_hours state["last_session_start"] = None + state["last_study_date"] = datetime.now().strftime("%Y-%m-%d") save_state(state) - print(f"Great Job! You studied for {duration:.2f} hours.") - print(f"Total study time: {state['total_study_time']:.2f} hours.") + print(f"Study session ended. Duration: {elapsed_hours:.2f} hours") + # Trigger pet update + update_pet() + print("Pet data updated!") -def get_total_time(): + +def reset_sessions(): """ - Returns the total accumulated study time (in hours). + For testing or restarting — clears active session flag. """ state = load_state() + state["last_session_start"] = None + save_state(state) + print("Session reset complete.") + + +def get_total_time(): + """Returns the total study time stored in JSON (for tests).""" + state = load_state() return state.get("total_study_time", 0.0) -# temp testing code +# Manual test if __name__ == "__main__": - print("=== Study Session Tracker Test ===") - start_session() - print("Simulating study for 3 seconds...") - time.sleep(10) # Simulate a study session of 10 seconds - end_session() - print("Total study time:", get_total_time(), "hours") + print("Study Tracker CLI") + print("1. start_session()") + print("2. end_session()") + print("3. reset_sessions()") + + +def _auto_end_session(*args): + state = load_state() + if state.get("last_session_start"): + print("\n Auto-saving your progress...") + end_session() + + +# autosave on exit or interrupt +atexit.register(_auto_end_session) +signal.signal(signal.SIGINT, _auto_end_session) +signal.signal(signal.SIGTERM, _auto_end_session) diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..d62552f --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,42 @@ +import sys, os, pytest +from datetime import datetime + +# 确保导入路径正确 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from study_pet.pet.core import update_pet, get_status +from study_pet.data_manager import load_state, save_state, reset_state + + +@pytest.fixture(autouse=True) +def clean_state(): + reset_state() + yield + reset_state() + + +def test_update_pet_increases_level(): + state = load_state() + state["total_study_time"] = 10.0 # 10小时 = 等级3 + save_state(state) + + new_state = update_pet() + assert new_state["level"] >= 3 + + +def test_update_pet_sets_last_study_date(): + update_pet() + state = load_state() + today = datetime.now().strftime("%Y-%m-%d") + assert state["last_study_date"] == today + + +def test_get_status_reflects_live_session_time(monkeypatch): + state = load_state() + state["total_study_time"] = 2.0 + state["last_session_start"] = 0 + save_state(state) + + monkeypatch.setattr("time.time", lambda: 3600) + status = get_status() + assert "3.00" in status or "2.99" in status diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 7e99a8f..1c6271e 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -5,7 +5,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from study_pet.tracker import start_session, end_session, get_total_time +from study_pet.tracker import start_session, end_session, get_total_time, reset_sessions from study_pet.data_manager import load_state, reset_state, save_state @@ -39,6 +39,14 @@ def test_get_total_time_matches_state(): assert abs(total_time - 12.34) < 0.001 +def test_reset_sessions_clears_active_session(): + + start_session() + reset_sessions() + state = load_state() + assert state["last_session_start"] is None + + def test_end_session_without_start_does_not_crash(): try: end_session() From d78bb984f788e1da89d21fad437d2ea14a043eff Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 21:09:22 -0500 Subject: [PATCH 10/40] export functions in init.py --- study_pet/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/study_pet/__init__.py b/study_pet/__init__.py index 0e9a102..436015a 100644 --- a/study_pet/__init__.py +++ b/study_pet/__init__.py @@ -1,2 +1,11 @@ -def hello(): - return "🐾 Hello from StudyPet!" +from .tracker import start_session, end_session +from .pet.core import get_status +from .data_manager import reset_state as _reset_state + +__all__ = ["start_session", "end_session", "get_status", "reset_pet"] + + +def reset_pet(): + """Resets all pet data and progress.""" + _reset_state() + print("Your pet has been reborn! All progress reset.") From b787f7c9bcceb1ebc1363e3285c6ee88fc4db6d8 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 21:11:58 -0500 Subject: [PATCH 11/40] export functions in init.py and update newest version --- study_pet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/study_pet/__init__.py b/study_pet/__init__.py index 436015a..5689d45 100644 --- a/study_pet/__init__.py +++ b/study_pet/__init__.py @@ -8,4 +8,4 @@ def reset_pet(): """Resets all pet data and progress.""" _reset_state() - print("Your pet has been reborn! All progress reset.") + print("🐣 Your pet has been reborn! All progress reset.") From dc69eddc6feb610e1cef934b1c95a8301e6d0364 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Mon, 3 Nov 2025 09:25:26 -0500 Subject: [PATCH 12/40] Add StudyPet interaction system and core updates --- Pipfile | 2 +- study_pet/__main__.py | 112 +++++++++++++++++++++++++++++ study_pet/data_manager.py | 4 ++ study_pet/pet/__init__.py | 11 ++- study_pet/pet/actions.py | 144 +++++++++++++++++++++++++++++++++++++ study_pet/pet/core.py | 101 ++++++++++++++++++++++++-- study_pet/tracker.py | 4 +- tests/test_actions.py | 62 ++++++++++++++++ tests/test_core.py | 16 ++++- tests/test_data_manager.py | 7 ++ tests/test_main.py | 30 ++++++++ tests/test_tracker.py | 18 ++++- 12 files changed, 499 insertions(+), 12 deletions(-) create mode 100644 study_pet/__main__.py create mode 100644 study_pet/pet/actions.py create mode 100644 tests/test_actions.py create mode 100644 tests/test_main.py diff --git a/Pipfile b/Pipfile index bd9ce6c..538ba9f 100644 --- a/Pipfile +++ b/Pipfile @@ -6,9 +6,9 @@ name = "pypi" [packages] [dev-packages] -pytest = "*" build = "*" twine = "*" +pytest = "*" [requires] python_version = "3.13" diff --git a/study_pet/__main__.py b/study_pet/__main__.py new file mode 100644 index 0000000..a6271da --- /dev/null +++ b/study_pet/__main__.py @@ -0,0 +1,112 @@ +from . import start_session, end_session, get_status, reset_pet +from .data_manager import load_state, save_state +from .pet import rename_pet, collect_money, feed_pet, check_daily_mood_decay +import argparse +import study_pet.tracker as tracker + + +def main(): + parser = argparse.ArgumentParser(description="🐾 StudyPet CLI") + parser.add_argument( + "command", + nargs="?", + default="menu", + help="Available commands: start, end, status, feed, menu", + ) + args = parser.parse_args() + + check_daily_mood_decay() + + if args.command == "start": + start_session() + elif args.command == "end": + end_session() + elif args.command == "status": + print(get_status()) + elif args.command == "feed": + feed_pet() + elif args.command == "menu": + main_menu() + else: + print("Unknown command. Use: start | end | status | feed | menu") + + +def actions_menu(): + """Submenu for all pet-related actions.""" + while True: + print("\nActions Menu:") + print("1. Collect coins ") + print("2. Feed your pet") + print("3. Back") + + choice = input("\nSelect an option (1–3): ").strip() + + if choice == "1": + collect_money() + elif choice == "2": + feed_pet() + elif choice == "3": + break + else: + print("Invalid option. Try again.") + + +def settings_menu(): + """Submenu for settings and info.""" + while True: + print("\nSettings Menu:") + print("1. Check pet status") + print("2. Rename your pet") + print("3. Reset all data") + print("4. Back") + + choice = input("\nSelect an option (1–4): ").strip() + + if choice == "1": + print(get_status()) + elif choice == "2": + rename_pet() + elif choice == "3": + confirm = input( + "This will reset all progress. Type 'byebye' to confirm " + ).lower() + if confirm == "byebye": + reset_pet() + elif choice == "4": + break + else: + print("Invalid option. Try again.") + + +def main_menu(): + """Main entry menu.""" + while True: + print("\n🐾 Welcome to StudyPet!\n") + print("1. Start studying ") + print("2. End session") + print("3. Actions") + print("4. Settings") + print("5. Close Menu (return to terminal)") + print("6. Exit (Close StudyPet to terminal)") + + choice = input("\nSelect an option (1–6): ").strip() + if choice == "1": + start_session() + elif choice == "2": + end_session() + elif choice == "3": + actions_menu() + elif choice == "4": + settings_menu() + elif choice == "5": + tracker.manual_close = True + break + elif choice == "6": + tracker.manual_close = False + break + else: + print("Invalid option. Try again.") + + +if __name__ == "__main__": + main() diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py index af4f11b..1d07080 100644 --- a/study_pet/data_manager.py +++ b/study_pet/data_manager.py @@ -13,6 +13,10 @@ "last_study_date": None, "mood": 100, "streak_days": 0, + "money": 0, + "last_collect_time": None, + "last_feed_date": None, + "last_open_date": None, } diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py index 4fd80df..e0a30cf 100644 --- a/study_pet/pet/__init__.py +++ b/study_pet/pet/__init__.py @@ -1,3 +1,10 @@ -from .core import update_pet, get_status +from .core import get_status, check_daily_mood_decay +from .actions import rename_pet, collect_money, feed_pet -__all__ = ["update_pet", "get_status"] +__all__ = [ + "get_status", + "rename_pet", + "collect_money", + "feed_pet", + "check_daily_mood_decay", +] diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py new file mode 100644 index 0000000..b211986 --- /dev/null +++ b/study_pet/pet/actions.py @@ -0,0 +1,144 @@ +""" +Pet interaction features (rename, feed, talk, etc.) +""" + +from datetime import datetime, timedelta +import time +from ..data_manager import load_state, save_state +import random + + +def rename_pet(new_name: str = None): + """ + Renames the pet and saves to state. + If no new_name is passed, will ask user input interactively. + """ + state = load_state() + old_name = state.get("name", "Unnamed") + + if not new_name: + print(f"\nCurrent name: {old_name}") + new_name = input("Enter new name for your pet: ").strip() + + if not new_name: + print("Name cannot be empty.") + return + + state["name"] = new_name + save_state(state) + print(f"Pet name changed to '{new_name}'!\n") + + +def collect_money(): + """ + Allows user to collect money once every 30 minutes. + Grants a random reward between 50–100 coins. + """ + state = load_state() + now = time.time() + name = state.get("name", "PomPom") + last_collect = state.get("last_collect_time", None) + + cooldown = 30 * 60 # 30 minutes in seconds + + if last_collect: + elapsed = now - last_collect + if elapsed < cooldown: + remaining = cooldown - elapsed + minutes = int(remaining // 60) + seconds = int(remaining % 60) + print(f"You can collect again in {minutes}m {seconds}s.") + return + + # reward logic + reward = random.randint(50, 100) + state["money"] = state.get("money", 0) + reward + state["last_collect_time"] = now + save_state(state) + + print(f"{name} found {reward} coins!") + print(f"Total balance: {state['money']} coins.") + + +def feed_pet(): + """ + Feed your pet with food purchased using money. + Each food has different cost and mood increase. + """ + state = load_state() + name = state.get("name", "PomPom") + money = state.get("money", 0) + mood = state.get("mood", 100) + + foods = { + "apple": {"cost": 80, "mood": 10, "emoji": "🍎", "msg": "Crunchy and sweet!"}, + "cake": { + "cost": 150, + "mood": 20, + "emoji": "🍰", + "msg": "So yummy! Sugar rush!", + }, + "coffee": {"cost": 50, "mood": 5, "emoji": "☕", "msg": "Ahh... more energy!"}, + "carrot": {"cost": 65, "mood": 8, "emoji": "🥕", "msg": "Healthy choice!"}, + "sushi": { + "cost": 130, + "mood": 15, + "emoji": "🍣", + "msg": "Delicious! I feel fancy!", + }, + "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Yum! That was tasty!"}, + } + print(f"\n{name}'s current mood: {mood}/100 😊") + print(f"Current balance: {money} coins 💰") + print("Choose something to feed your pet:") + print("1. Apple 🍎 (Cost: 80 | +10 mood)") + print("2. Cake 🍰 (Cost: 150 | +20 mood)") + print("3. Coffee ☕ (Cost: 50 | +5 mood)") + print("4. Carrot 🥕 (Cost: 65 | +8 mood)") + print("5. Sushi 🍣 (Cost: 130 | +15 mood)") + print("6. Custom food ✏️ (Cost: 80 | +10 mood)") + print("7. Return") + choice = input("Select (1–7): ").strip() + + mapping = { + "1": "apple", + "2": "cake", + "3": "coffee", + "4": "carrot", + "5": "sushi", + "6": "custom", + } + if choice == "7": + return + if choice not in mapping: + print(" Invalid choice.") + return + + selected = mapping[choice] + food = foods[selected] + + # handle custom name + if selected == "custom": + custom_name = input("Enter your custom food name: ").strip() or "mystery meal" + food["msg"] = f"{name} happily ate your {custom_name}!" + display_name = custom_name + else: + display_name = selected + + # check balance + if money < food["cost"]: + print(f"Not enough coins! {food['cost']} needed, but you have {money}.") + return + + # apply effects + money -= food["cost"] + new_mood = min(100, mood + food["mood"]) + state["money"] = money + state["mood"] = new_mood + state["last_feed_date"] = datetime.now().strftime("%Y-%m-%d") + save_state(state) + + print(f"\n{food['emoji']} You fed {name} a {display_name}!") + print(food["msg"]) + print(f" Mood increased to {new_mood}/100.") + print(f" Remaining balance: {money} coins.\n") diff --git a/study_pet/pet/core.py b/study_pet/pet/core.py index d783f25..639d2e0 100644 --- a/study_pet/pet/core.py +++ b/study_pet/pet/core.py @@ -73,24 +73,117 @@ def get_status(): name = state.get("name", "Unnamed") mood = state.get("mood", 100) + money = state.get("money", 0) + streak = state.get("streak_days", 0) last_study = state.get("last_study_date", "N/A") studying = "Studying now" if last_start else " Idle" + if mood >= 80: + mood_status = "😊 Very happy!" + elif mood >= 60: + mood_status = "😌 Content." + elif mood >= 40: + mood_status = "😕 A bit tired..." + elif mood >= 20: + mood_status = "😣 Needs care soon!" + else: + mood_status = "😭 Very sad!" + status = ( - f"\n Pet Status (Live Preview)\n" - f"Name: {name}\n" + f"\n Pet Status \n" + f"--------------------------------\n" + f"{name}\n" f"Level: {level} ({exp:.0f} EXP)\n" f"Total Study Time: {total_time:.2f} hrs (+{elapsed:.2f}h current)\n" f"Last Study: {last_study}\n" - f"Mood: {mood}/100 Streak: {streak} days\n" - f"Session: {studying}" + f"\nMood: {mood}/100 — {mood_status}\n" + f"Money: {money} coins\n" + f"Streak: {streak} days\n" + f"\nSession: {studying}\n" + f"--------------------------------" ) return status +def check_daily_mood_decay(): + """ + Called at program startup. + Decreases pet mood based on days since last login or last feeding. + """ + state = load_state() + + last_open_str = state.get("last_open_date") + last_feed_str = state.get("last_feed_date") + mood = state.get("mood", 100) + + today = datetime.now().date() + last_open = None + last_feed = None + + # convert stored dates + if last_open_str: + try: + last_open = datetime.strptime(last_open_str, "%Y-%m-%d").date() + except ValueError: + last_open = today + else: + last_open = today + + if last_feed_str: + try: + last_feed = datetime.strptime(last_feed_str, "%Y-%m-%d").date() + except ValueError: + last_feed = today + + # calculate days passed since last open + days_passed = (today - last_open).days + if days_passed <= 0: + # same day login, no decay + state["last_open_date"] = today.strftime("%Y-%m-%d") + save_state(state) + return + + # base decay by days + if days_passed == 1: + decay = 5 + elif days_passed == 2: + decay = 15 + elif days_passed == 3: + decay = 35 + elif days_passed == 4: + decay = 60 + else: + # 5 or more days + decay = 999 # will drop to 0 anyway + + # extra penalty if not fed yesterday + if last_feed is None or (today - last_feed).days >= 1: + decay += 10 + + # apply decay + new_mood = max(0, mood - decay) + + # update state + state["mood"] = new_mood + state["last_open_date"] = today.strftime("%Y-%m-%d") + save_state(state) + + # feedback message + if decay > 0: + print( + f"\nIt's been {days_passed} day(s) since you last visited." + f"\nYour pet’s mood decreased by {decay} → now {new_mood}/100 💖" + ) + + if new_mood == 0: + print("Your pet is very sad... please feed it soon!") + + return new_mood + + # Manual test if __name__ == "__main__": print("🔁 Checking pet status preview...") diff --git a/study_pet/tracker.py b/study_pet/tracker.py index 9f81112..98ca58d 100644 --- a/study_pet/tracker.py +++ b/study_pet/tracker.py @@ -83,10 +83,12 @@ def get_total_time(): print("2. end_session()") print("3. reset_sessions()") +manual_close = False + def _auto_end_session(*args): state = load_state() - if state.get("last_session_start"): + if state.get("last_session_start") and not manual_close: print("\n Auto-saving your progress...") end_session() diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 0000000..cde2266 --- /dev/null +++ b/tests/test_actions.py @@ -0,0 +1,62 @@ +import sys, os, time, pytest, builtins +from datetime import datetime + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from study_pet.pet.actions import rename_pet, collect_money, feed_pet +from study_pet.data_manager import load_state, save_state, reset_state + + +@pytest.fixture(autouse=True) +def clean_state(): + reset_state() + yield + reset_state() + + +def test_rename_pet_interactive(monkeypatch): + monkeypatch.setattr(builtins, "input", lambda _: "Fluffy") + rename_pet() + state = load_state() + assert state["name"] == "Fluffy" + + +def test_collect_money_adds_balance(monkeypatch): + state = load_state() + start_money = state["money"] + collect_money() + state = load_state() + assert state["money"] > start_money + + +def test_collect_money_respects_cooldown(monkeypatch, capsys): + state = load_state() + state["last_collect_time"] = time.time() + save_state(state) + collect_money() + captured = capsys.readouterr().out + assert "You can collect again" in captured + + +def test_feed_pet_increases_mood(monkeypatch): + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + inputs = iter(["1"]) # Apple + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + feed_pet() + new_state = load_state() + assert new_state["mood"] > 60 + + +def test_feed_pet_insufficient_funds(monkeypatch, capsys): + state = load_state() + state["money"] = 0 + save_state(state) + inputs = iter(["1"]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + feed_pet() + captured = capsys.readouterr().out + assert "Not enough coins" in captured diff --git a/tests/test_core.py b/tests/test_core.py index d62552f..c50b38b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,10 +1,10 @@ import sys, os, pytest from datetime import datetime -# 确保导入路径正确 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from study_pet.pet.core import update_pet, get_status +from datetime import timedelta +from study_pet.pet.core import update_pet, get_status, check_daily_mood_decay from study_pet.data_manager import load_state, save_state, reset_state @@ -17,7 +17,7 @@ def clean_state(): def test_update_pet_increases_level(): state = load_state() - state["total_study_time"] = 10.0 # 10小时 = 等级3 + state["total_study_time"] = 10.0 # 、 save_state(state) new_state = update_pet() @@ -40,3 +40,13 @@ def test_get_status_reflects_live_session_time(monkeypatch): monkeypatch.setattr("time.time", lambda: 3600) status = get_status() assert "3.00" in status or "2.99" in status + + +def test_check_daily_mood_decay_reduces_mood(): + s = load_state() + s["mood"] = 80 + old_date = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d") + s["last_open_date"] = old_date + save_state(s) + new_mood = check_daily_mood_decay() + assert new_mood < 80 diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py index 26c8fbf..4075fa0 100644 --- a/tests/test_data_manager.py +++ b/tests/test_data_manager.py @@ -39,3 +39,10 @@ def test_reset_state_resets_to_default(): state = dm.load_state() assert state["name"] == "PomPom" assert state["level"] == 1 + + +def test_save_and_reload(): + s = {"name": "A", "level": 5} + dm.save_state(s) + re = dm.load_state() + assert re["name"] == "A" diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..d974370 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,30 @@ +import sys, os, pytest + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from study_pet.__main__ import main + + +def test_main_start(monkeypatch): + calls = {"started": False} + monkeypatch.setattr( + "study_pet.__main__.start_session", lambda: calls.update(started=True) + ) + monkeypatch.setattr("sys.argv", ["prog", "start"]) + main() + assert calls["started"] + + +def test_main_status(monkeypatch, capsys): + monkeypatch.setattr("sys.argv", ["prog", "status"]) + monkeypatch.setattr("study_pet.__main__.get_status", lambda: "Pet OK") + main() + captured = capsys.readouterr().out + assert "Pet OK" in captured + + +def test_main_invalid_command(monkeypatch, capsys): + monkeypatch.setattr("sys.argv", ["prog", "unknown"]) + main() + captured = capsys.readouterr().out + assert "Unknown command" in captured diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 1c6271e..90e1eeb 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -5,7 +5,14 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from study_pet.tracker import start_session, end_session, get_total_time, reset_sessions +from study_pet.tracker import ( + start_session, + end_session, + get_total_time, + reset_sessions, + manual_close, + _auto_end_session, +) from study_pet.data_manager import load_state, reset_state, save_state @@ -53,3 +60,12 @@ def test_end_session_without_start_does_not_crash(): assert True except Exception as e: pytest.fail(f"end_session() raised an unexpected exception: {e}") + + +def test_auto_end_session_respects_manual_close(monkeypatch): + start_session() + monkeypatch.setattr("study_pet.tracker.manual_close", True) + _auto_end_session() + state = load_state() + # still active because manual close skipped + assert state["last_session_start"] is not None From 09f0d11bc6a258d032419adf85e72c7f3ba12d51 Mon Sep 17 00:00:00 2001 From: catherineyu2014 Date: Mon, 3 Nov 2025 10:57:46 -0500 Subject: [PATCH 13/40] add additional cases for pet's actions --- tests/test_actions.py | 81 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index cde2266..850b19e 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -14,13 +14,32 @@ def clean_state(): reset_state() +# rename_pet(): correct def test_rename_pet_interactive(monkeypatch): monkeypatch.setattr(builtins, "input", lambda _: "Fluffy") rename_pet() state = load_state() assert state["name"] == "Fluffy" +# rename_pet(): invalid input +def test_rename_pet_empty(monkeypatch, capsys): + # name should be unchanged if user input is empty + state = load_state() + state["name"] = "Fluffy" + save_state(state) + + monkeypatch.setattr(builtins, "input", lambda _: "") + rename_pet() + + captured = capsys.readouterr().out + new_state = load_state() + + + assert "Name cannot be empty." in captured + assert new_state["name"] == "Fluffy" + +# collect_money(): correct case def test_collect_money_adds_balance(monkeypatch): state = load_state() start_money = state["money"] @@ -28,7 +47,7 @@ def test_collect_money_adds_balance(monkeypatch): state = load_state() assert state["money"] > start_money - +# collect_money(): invalid case def test_collect_money_respects_cooldown(monkeypatch, capsys): state = load_state() state["last_collect_time"] = time.time() @@ -37,7 +56,7 @@ def test_collect_money_respects_cooldown(monkeypatch, capsys): captured = capsys.readouterr().out assert "You can collect again" in captured - +# feed_pet(): correct case def test_feed_pet_increases_mood(monkeypatch): state = load_state() state["money"] = 500 @@ -50,7 +69,7 @@ def test_feed_pet_increases_mood(monkeypatch): new_state = load_state() assert new_state["mood"] > 60 - +# feed_pet(): invalid case - insufficient funds def test_feed_pet_insufficient_funds(monkeypatch, capsys): state = load_state() state["money"] = 0 @@ -60,3 +79,59 @@ def test_feed_pet_insufficient_funds(monkeypatch, capsys): feed_pet() captured = capsys.readouterr().out assert "Not enough coins" in captured + +# feed_pet(): invalid case - invalid menu item +def test_feed_pet_invalid_choice(monkeypatch, capsys): + state = load_state() + state["money"] = 500 + save_state(state) + + monkeypatch.setattr(builtins, "input", lambda _: "9") + feed_pet() + + captured = capsys.readouterr().out + assert "Invalid choice" in captured + +# feed_pet(): menu item 6 - custom food success +def test_feed_pet_custom_food(monkeypatch): + state = load_state() + state["money"] = 500 + state["mood"] = 80 + save_state(state) + + inputs = iter(["6", "pancakes"]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + + feed_pet() + new_state = load_state() + assert new_state["mood"] > 80 + assert new_state["money"] < 500 + +# feed_pet(): menu item 6 - custom food empty +def test_feed_pet_custom_food_empty_name(monkeypatch, capsys): + # if user input is empty, mystery meal + state = load_state() + state["money"] = 500 + save_state(state) + + inputs = iter(["6", ""]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + + feed_pet() + captured = capsys.readouterr().out + assert "mystery meal" in captured + +# feed_pet(): menu item 7 - return +def test_feed_pet_return(monkeypatch): + # no state change should happen + state = load_state() + state["money"] = 500 + state["mood"] = 50 + save_state(state) + + monkeypatch.setattr(builtins, "input", lambda _: "7") + feed_pet() + new_state = load_state() + + assert new_state["money"] == 500 + assert new_state["mood"] == 50 \ No newline at end of file From bc44beb0682c57c6549248cdeaf92de9509e125e Mon Sep 17 00:00:00 2001 From: catherineyu2014 Date: Mon, 3 Nov 2025 11:49:54 -0500 Subject: [PATCH 14/40] added unit tests for main --- tests/test_core.py | 2 +- tests/test_main.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index c50b38b..55702b2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -17,7 +17,7 @@ def clean_state(): def test_update_pet_increases_level(): state = load_state() - state["total_study_time"] = 10.0 # 、 + state["total_study_time"] = 10.0 save_state(state) new_state = update_pet() diff --git a/tests/test_main.py b/tests/test_main.py index d974370..f7fa9b0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,7 +4,7 @@ from study_pet.__main__ import main - +# start option def test_main_start(monkeypatch): calls = {"started": False} monkeypatch.setattr( @@ -14,7 +14,17 @@ def test_main_start(monkeypatch): main() assert calls["started"] +# end option +def test_main_end(monkeypatch): + calls = {"ended": False} + monkeypatch.setattr( + "study_pet.__main__.end_session", lambda: calls.update(ended=True) + ) + monkeypatch.setattr("sys.argv", ["prog", "end"]) + main() + assert calls["ended"] +# status option def test_main_status(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["prog", "status"]) monkeypatch.setattr("study_pet.__main__.get_status", lambda: "Pet OK") @@ -22,9 +32,29 @@ def test_main_status(monkeypatch, capsys): captured = capsys.readouterr().out assert "Pet OK" in captured +# feed option +def test_main_feed(monkeypatch): + calls = {"fed": False} + monkeypatch.setattr( + "study_pet.__main__.feed_pet", lambda: calls.update(fed=True) + ) + monkeypatch.setattr("sys.argv", ["prog", "feed"]) + main() + assert calls["fed"] +# unknown command def test_main_invalid_command(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["prog", "unknown"]) main() captured = capsys.readouterr().out assert "Unknown command" in captured + +# two tries +def test_main_menu_invalid_then_exit(monkeypatch, capsys): + monkeypatch.setattr("sys.argv", ["prog"]) # main menu + inputs = iter(["invalid", "5"]) # 1) invalid menu choice, then 2) exit + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + + main() + captured = capsys.readouterr().out + assert "Invalid option. Try again." in captured \ No newline at end of file From 80cdd8633ef56cfba28bd3ab583c82ea221f190e Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 21:00:16 +0400 Subject: [PATCH 15/40] readme update --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6022e0e..1d008e5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ -# Python Package Exercise +# StudyPet -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. +A virtual pet companion that grows with you as you study. Track your learning sessions, watch your pet level up, and stay motivated with a gamified study routine. Every hour you spend learning helps your pet grow stronger and happier. + +## Installation + +Install from PyPI: + +```bash +pip install study-pet +``` + +Or install from source: + +```bash +git clone https://github.com/YOUR_USERNAME/team_cascade.git +cd team_cascade +pipenv install --dev +``` + +## Team + +- [Leo Fu](https://github.com/https://github.com/LeoFYH) From 82482f09fccb2271f4fa7552561bd45593f00de4 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 12:21:42 -0500 Subject: [PATCH 16/40] Readme edits --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d008e5..a7f349f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ +[![CI](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml/badge.svg?branch=pipfile-experiment)](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml) + +````markdown # StudyPet +## Team Members -A virtual pet companion that grows with you as you study. Track your learning sessions, watch your pet level up, and stay motivated with a gamified study routine. Every hour you spend learning helps your pet grow stronger and happier. +- [Catherine Yu](https://github.com/catherineyu2014) +- [Leo Fu](https://github.com/LeoFYH) +- [JunHao Chen](https://github.com/JunHaoChen16) +- [Majo Salgado](https://github.com/mariajsalgadoq) +- [Zeba Shafi](https://github.com/Zeba-Shafi) ## Installation From d24cdd97001095448cea9447404c0ad84e5de4a0 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 12:21:59 -0500 Subject: [PATCH 17/40] Readme edits --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a7f349f..5d0665d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ````markdown # StudyPet + ## Team Members - [Catherine Yu](https://github.com/catherineyu2014) From 56b4bb9124e480fa7e7cc5b243bc95984db5a916 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 22:13:38 +0400 Subject: [PATCH 18/40] renamePet parameter supported --- study_pet/__main__.py | 60 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/study_pet/__main__.py b/study_pet/__main__.py index a6271da..50b9f31 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -7,13 +7,52 @@ def main(): parser = argparse.ArgumentParser(description="🐾 StudyPet CLI") - parser.add_argument( - "command", - nargs="?", - default="menu", - help="Available commands: start, end, status, feed, menu", + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Start command + subparsers.add_parser("start", help="Start a study session") + + # End command + subparsers.add_parser("end", help="End current study session") + + # Status command + subparsers.add_parser("status", help="Check pet status") + + # Feed command + feed_parser = subparsers.add_parser("feed", help="Feed your pet") + feed_parser.add_argument( + "--food-type", + choices=["apple", "cake", "coffee", "carrot", "sushi", "custom"], + help="Food type to feed directly (optional, shows menu if not provided)", ) + feed_parser.add_argument( + "--custom-name", + help="Custom food name (only used with --food-type custom)", + ) + + # Collect command + collect_parser = subparsers.add_parser("collect", help="Collect coins") + collect_parser.add_argument( + "--force", + action="store_true", + help="Force collection, bypass cooldown (for testing)", + ) + + # Rename command + rename_parser = subparsers.add_parser("rename", help="Rename your pet") + rename_parser.add_argument( + "--name", + help="New name for your pet (optional, will prompt if not provided)", + ) + + # Menu command (default) + subparsers.add_parser("menu", help="Open interactive menu") + args = parser.parse_args() + + # Default to menu if no command provided + if not args.command: + args.command = "menu" check_daily_mood_decay() @@ -24,11 +63,18 @@ def main(): elif args.command == "status": print(get_status()) elif args.command == "feed": - feed_pet() + feed_pet( + food_type=getattr(args, 'food_type', None), + custom_name=getattr(args, 'custom_name', None), + ) + elif args.command == "collect": + collect_money(force=getattr(args, 'force', False)) + elif args.command == "rename": + rename_pet(new_name=getattr(args, 'name', None)) elif args.command == "menu": main_menu() else: - print("Unknown command. Use: start | end | status | feed | menu") + parser.print_help() def actions_menu(): From 7038f5ee7dd7068c8f8323fc400b40a75ac91b64 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 22:16:39 +0400 Subject: [PATCH 19/40] feedPet parameter supported --- study_pet/__main__.py | 9 +----- study_pet/pet/actions.py | 68 ++++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 50b9f31..0118bc3 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -25,10 +25,6 @@ def main(): choices=["apple", "cake", "coffee", "carrot", "sushi", "custom"], help="Food type to feed directly (optional, shows menu if not provided)", ) - feed_parser.add_argument( - "--custom-name", - help="Custom food name (only used with --food-type custom)", - ) # Collect command collect_parser = subparsers.add_parser("collect", help="Collect coins") @@ -63,10 +59,7 @@ def main(): elif args.command == "status": print(get_status()) elif args.command == "feed": - feed_pet( - food_type=getattr(args, 'food_type', None), - custom_name=getattr(args, 'custom_name', None), - ) + feed_pet(food_type=getattr(args, 'food_type', None)) elif args.command == "collect": collect_money(force=getattr(args, 'force', False)) elif args.command == "rename": diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index b211986..89cfb34 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -60,10 +60,14 @@ def collect_money(): print(f"Total balance: {state['money']} coins.") -def feed_pet(): +def feed_pet(food_type: str = None): """ Feed your pet with food purchased using money. Each food has different cost and mood increase. + + Args: + food_type: Optional food type to feed directly. Valid values: "apple", "cake", + "coffee", "carrot", "sushi", "custom". If None, shows interactive menu. """ state = load_state() name = state.get("name", "PomPom") @@ -88,33 +92,43 @@ def feed_pet(): }, "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Yum! That was tasty!"}, } - print(f"\n{name}'s current mood: {mood}/100 😊") - print(f"Current balance: {money} coins 💰") - print("Choose something to feed your pet:") - print("1. Apple 🍎 (Cost: 80 | +10 mood)") - print("2. Cake 🍰 (Cost: 150 | +20 mood)") - print("3. Coffee ☕ (Cost: 50 | +5 mood)") - print("4. Carrot 🥕 (Cost: 65 | +8 mood)") - print("5. Sushi 🍣 (Cost: 130 | +15 mood)") - print("6. Custom food ✏️ (Cost: 80 | +10 mood)") - print("7. Return") - choice = input("Select (1–7): ").strip() - - mapping = { - "1": "apple", - "2": "cake", - "3": "coffee", - "4": "carrot", - "5": "sushi", - "6": "custom", - } - if choice == "7": - return - if choice not in mapping: - print(" Invalid choice.") - return + + # If food_type provided, use it directly + if food_type: + food_type = food_type.lower() + if food_type not in foods: + print(f"Invalid food type: {food_type}. Valid options: {', '.join(foods.keys())}") + return + selected = food_type + else: + # Interactive mode + print(f"\n{name}'s current mood: {mood}/100 😊") + print(f"Current balance: {money} coins 💰") + print("Choose something to feed your pet:") + print("1. Apple 🍎 (Cost: 80 | +10 mood)") + print("2. Cake 🍰 (Cost: 150 | +20 mood)") + print("3. Coffee ☕ (Cost: 50 | +5 mood)") + print("4. Carrot 🥕 (Cost: 65 | +8 mood)") + print("5. Sushi 🍣 (Cost: 130 | +15 mood)") + print("6. Custom food ✏️ (Cost: 80 | +10 mood)") + print("7. Return") + choice = input("Select (1–7): ").strip() + + mapping = { + "1": "apple", + "2": "cake", + "3": "coffee", + "4": "carrot", + "5": "sushi", + "6": "custom", + } + if choice == "7": + return + if choice not in mapping: + print(" Invalid choice.") + return + selected = mapping[choice] - selected = mapping[choice] food = foods[selected] # handle custom name From dfbf7f7b0d8bc2c1c2371ec58b665dbd78e60acb Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 22:33:44 +0400 Subject: [PATCH 20/40] Revert "Merge pull request #9 from LeoFYH/pipfile-experiment" This reverts commit 6d503dac8d7920d582010f76ea8e4a0b72a8f3ba, reversing :wqchanges made to d24cdd97001095448cea9447404c0ad84e5de4a0. :wq# Your branch is up to date with 'origin/pipfile-experiment'. --- study_pet/__main__.py | 53 +++++-------------------------- study_pet/pet/actions.py | 68 ++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 87 deletions(-) diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 0118bc3..a6271da 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -7,48 +7,13 @@ def main(): parser = argparse.ArgumentParser(description="🐾 StudyPet CLI") - subparsers = parser.add_subparsers(dest="command", help="Available commands") - - # Start command - subparsers.add_parser("start", help="Start a study session") - - # End command - subparsers.add_parser("end", help="End current study session") - - # Status command - subparsers.add_parser("status", help="Check pet status") - - # Feed command - feed_parser = subparsers.add_parser("feed", help="Feed your pet") - feed_parser.add_argument( - "--food-type", - choices=["apple", "cake", "coffee", "carrot", "sushi", "custom"], - help="Food type to feed directly (optional, shows menu if not provided)", + parser.add_argument( + "command", + nargs="?", + default="menu", + help="Available commands: start, end, status, feed, menu", ) - - # Collect command - collect_parser = subparsers.add_parser("collect", help="Collect coins") - collect_parser.add_argument( - "--force", - action="store_true", - help="Force collection, bypass cooldown (for testing)", - ) - - # Rename command - rename_parser = subparsers.add_parser("rename", help="Rename your pet") - rename_parser.add_argument( - "--name", - help="New name for your pet (optional, will prompt if not provided)", - ) - - # Menu command (default) - subparsers.add_parser("menu", help="Open interactive menu") - args = parser.parse_args() - - # Default to menu if no command provided - if not args.command: - args.command = "menu" check_daily_mood_decay() @@ -59,15 +24,11 @@ def main(): elif args.command == "status": print(get_status()) elif args.command == "feed": - feed_pet(food_type=getattr(args, 'food_type', None)) - elif args.command == "collect": - collect_money(force=getattr(args, 'force', False)) - elif args.command == "rename": - rename_pet(new_name=getattr(args, 'name', None)) + feed_pet() elif args.command == "menu": main_menu() else: - parser.print_help() + print("Unknown command. Use: start | end | status | feed | menu") def actions_menu(): diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 89cfb34..b211986 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -60,14 +60,10 @@ def collect_money(): print(f"Total balance: {state['money']} coins.") -def feed_pet(food_type: str = None): +def feed_pet(): """ Feed your pet with food purchased using money. Each food has different cost and mood increase. - - Args: - food_type: Optional food type to feed directly. Valid values: "apple", "cake", - "coffee", "carrot", "sushi", "custom". If None, shows interactive menu. """ state = load_state() name = state.get("name", "PomPom") @@ -92,43 +88,33 @@ def feed_pet(food_type: str = None): }, "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Yum! That was tasty!"}, } - - # If food_type provided, use it directly - if food_type: - food_type = food_type.lower() - if food_type not in foods: - print(f"Invalid food type: {food_type}. Valid options: {', '.join(foods.keys())}") - return - selected = food_type - else: - # Interactive mode - print(f"\n{name}'s current mood: {mood}/100 😊") - print(f"Current balance: {money} coins 💰") - print("Choose something to feed your pet:") - print("1. Apple 🍎 (Cost: 80 | +10 mood)") - print("2. Cake 🍰 (Cost: 150 | +20 mood)") - print("3. Coffee ☕ (Cost: 50 | +5 mood)") - print("4. Carrot 🥕 (Cost: 65 | +8 mood)") - print("5. Sushi 🍣 (Cost: 130 | +15 mood)") - print("6. Custom food ✏️ (Cost: 80 | +10 mood)") - print("7. Return") - choice = input("Select (1–7): ").strip() - - mapping = { - "1": "apple", - "2": "cake", - "3": "coffee", - "4": "carrot", - "5": "sushi", - "6": "custom", - } - if choice == "7": - return - if choice not in mapping: - print(" Invalid choice.") - return - selected = mapping[choice] + print(f"\n{name}'s current mood: {mood}/100 😊") + print(f"Current balance: {money} coins 💰") + print("Choose something to feed your pet:") + print("1. Apple 🍎 (Cost: 80 | +10 mood)") + print("2. Cake 🍰 (Cost: 150 | +20 mood)") + print("3. Coffee ☕ (Cost: 50 | +5 mood)") + print("4. Carrot 🥕 (Cost: 65 | +8 mood)") + print("5. Sushi 🍣 (Cost: 130 | +15 mood)") + print("6. Custom food ✏️ (Cost: 80 | +10 mood)") + print("7. Return") + choice = input("Select (1–7): ").strip() + + mapping = { + "1": "apple", + "2": "cake", + "3": "coffee", + "4": "carrot", + "5": "sushi", + "6": "custom", + } + if choice == "7": + return + if choice not in mapping: + print(" Invalid choice.") + return + selected = mapping[choice] food = foods[selected] # handle custom name From 81fbea2367ce3e1fb2588852d0264b7dcbe8f505 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 14:02:20 -0500 Subject: [PATCH 21/40] test --- study_pet/data_manager.py | 2 +- study_pet/pet/actions.py | 4 ++-- tests/test_data_manager.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py index 1d07080..7e77e55 100644 --- a/study_pet/data_manager.py +++ b/study_pet/data_manager.py @@ -5,7 +5,7 @@ DATA_PATH = os.path.join(DATA_DIR, "data.json") default_state = { - "name": "PomPom", + "name": "Guido", "level": 1, "experience": 0.0, "total_study_time": 0.0, diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index b211986..6efa2dd 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -36,7 +36,7 @@ def collect_money(): """ state = load_state() now = time.time() - name = state.get("name", "PomPom") + name = state.get("name", "Guido") last_collect = state.get("last_collect_time", None) cooldown = 30 * 60 # 30 minutes in seconds @@ -66,7 +66,7 @@ def feed_pet(): Each food has different cost and mood increase. """ state = load_state() - name = state.get("name", "PomPom") + name = state.get("name", "Guido") money = state.get("money", 0) mood = state.get("mood", 100) diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py index 4075fa0..62665b5 100644 --- a/tests/test_data_manager.py +++ b/tests/test_data_manager.py @@ -37,7 +37,7 @@ def test_reset_state_resets_to_default(): dm.save_state({"name": "WrongPet", "level": 10}) dm.reset_state() state = dm.load_state() - assert state["name"] == "PomPom" + assert state["name"] == "Guido" assert state["level"] == 1 From 9dc7f2bf9aa22475455a1b3dd0c6102d8b7a2704 Mon Sep 17 00:00:00 2001 From: catherineyu2014 <60550034+catherineyu2014@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:18:46 -0500 Subject: [PATCH 22/40] Refactor README to remove team section Removed redundant team section and cleaned up README. --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 5d0665d..fb79515 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![CI](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml/badge.svg?branch=pipfile-experiment)](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml) -````markdown # StudyPet ## Team Members @@ -26,7 +25,3 @@ git clone https://github.com/YOUR_USERNAME/team_cascade.git cd team_cascade pipenv install --dev ``` - -## Team - -- [Leo Fu](https://github.com/https://github.com/LeoFYH) From c4f3576f2722171e8cc616b9fc90964421b53e16 Mon Sep 17 00:00:00 2001 From: catherineyu2014 <60550034+catherineyu2014@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:39:03 -0500 Subject: [PATCH 23/40] Update repository URL in installation instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb79515..dc2a69b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ pip install study-pet Or install from source: ```bash -git clone https://github.com/YOUR_USERNAME/team_cascade.git +git clone https://github.com/swe-students-fall2025/3-python-package-team_cascade.git cd team_cascade pipenv install --dev ``` From 280bf55eb6bd9b5ea0df905800cc2e0c5c2456f0 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 14:43:38 -0500 Subject: [PATCH 24/40] Readme edit --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index dc2a69b..28b409c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ # StudyPet +**StudyPet** is a Python package that provides an interactive command-line application to gamify your study sessions by letting you raise and care for a virtual pet. The more you study, the more your pet grows! Track your study time, earn coins, feed your pet, and watch it level up as you build consistent study habits. + +## Features + +- **📚 Study Session Tracking**: Start and end study sessions with automatic time tracking +- **🐾 Virtual Pet System**: Your pet levels up based on total study hours (1 level per 5 hours) +- **😊 Mood System**: Keep your pet happy by logging in regularly and feeding it +- **💰 Coin Collection**: Earn coins every 30 minutes to purchase food for your pet +- **🍰 Feed Your Pet**: Choose from various foods (apple, cake, coffee, sushi, and more) to boost mood +- **📈 Progress Tracking**: Monitor your study streaks, total study time, and pet status +- **🔄 Auto-save**: Sessions automatically save on exit or interruption +- **🎮 Interactive Menu**: Easy-to-use CLI menu for all features + ## Team Members - [Catherine Yu](https://github.com/catherineyu2014) From 4e94bef98a82d9794db9b0dfe53fb5a35365eaa6 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 23:48:56 +0400 Subject: [PATCH 25/40] rename&feed --- study_pet/__main__.py | 14 +++++++++--- study_pet/pet/actions.py | 39 +++++++++++++++++++++++++++++++- tests/test_actions.py | 48 ++++++++++++++++++++++++++++++++++++++-- tests/test_main.py | 22 +++++++++++++++++- 4 files changed, 116 insertions(+), 7 deletions(-) diff --git a/study_pet/__main__.py b/study_pet/__main__.py index a6271da..97fc387 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -11,7 +11,13 @@ def main(): "command", nargs="?", default="menu", - help="Available commands: start, end, status, feed, menu", + help="Available commands: start, end, status, feed, rename, menu", + ) + parser.add_argument( + "arg", + nargs="?", + default=None, + help="Optional argument for feed (food name) or rename (new name)", ) args = parser.parse_args() @@ -24,11 +30,13 @@ def main(): elif args.command == "status": print(get_status()) elif args.command == "feed": - feed_pet() + feed_pet(args.arg) + elif args.command == "rename": + rename_pet(args.arg) elif args.command == "menu": main_menu() else: - print("Unknown command. Use: start | end | status | feed | menu") + print("Unknown command. Use: start | end | status | feed [food] | rename [name] | menu") def actions_menu(): diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 6efa2dd..7ca9e3b 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -60,10 +60,14 @@ def collect_money(): print(f"Total balance: {state['money']} coins.") -def feed_pet(): +def feed_pet(food_name: str = None): """ Feed your pet with food purchased using money. Each food has different cost and mood increase. + + Args: + food_name: Optional food name (apple, cake, coffee, carrot, sushi). + If not provided, will show interactive menu. """ state = load_state() name = state.get("name", "Guido") @@ -88,6 +92,39 @@ def feed_pet(): }, "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Yum! That was tasty!"}, } + + # If food_name is provided, use it directly + if food_name: + food_name = food_name.lower().strip() + if food_name not in foods: + print(f"Invalid food name: {food_name}") + print(f"Available foods: {', '.join([f for f in foods.keys() if f != 'custom'])}") + return + + selected = food_name + food = foods[selected] + display_name = selected + + # check balance + if money < food["cost"]: + print(f"Not enough coins! {food['cost']} needed, but you have {money}.") + return + + # apply effects + money -= food["cost"] + new_mood = min(100, mood + food["mood"]) + state["money"] = money + state["mood"] = new_mood + state["last_feed_date"] = datetime.now().strftime("%Y-%m-%d") + save_state(state) + + print(f"\n{food['emoji']} You fed {name} a {display_name}!") + print(food["msg"]) + print(f" Mood increased to {new_mood}/100.") + print(f" Remaining balance: {money} coins.\n") + return + + # Interactive menu mode (original behavior) print(f"\n{name}'s current mood: {mood}/100 😊") print(f"Current balance: {money} coins 💰") print("Choose something to feed your pet:") diff --git a/tests/test_actions.py b/tests/test_actions.py index 850b19e..032b613 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -36,7 +36,20 @@ def test_rename_pet_empty(monkeypatch, capsys): assert "Name cannot be empty." in captured - assert new_state["name"] == "Fluffy" + assert new_state["name"] == "Fluffy" + +# rename_pet(): with parameter +def test_rename_pet_with_parameter(capsys): + state = load_state() + state["name"] = "OldName" + save_state(state) + + rename_pet("NewName") + + new_state = load_state() + captured = capsys.readouterr().out + assert new_state["name"] == "NewName" + assert "Pet name changed to 'NewName'!" in captured # collect_money(): correct case @@ -134,4 +147,35 @@ def test_feed_pet_return(monkeypatch): new_state = load_state() assert new_state["money"] == 500 - assert new_state["mood"] == 50 \ No newline at end of file + assert new_state["mood"] == 50 + +# feed_pet(): with parameter - apple +def test_feed_pet_with_parameter_apple(capsys): + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("apple") + + new_state = load_state() + captured = capsys.readouterr().out + assert new_state["mood"] == 70 # 60 + 10 + assert new_state["money"] == 420 # 500 - 80 + assert "apple" in captured.lower() + assert "🍎" in captured + +# feed_pet(): with parameter - invalid food name +def test_feed_pet_invalid_food_name(capsys): + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("invalid_food") + + new_state = load_state() + captured = capsys.readouterr().out + assert new_state["mood"] == 60 # unchanged + assert new_state["money"] == 500 # unchanged + assert "Invalid food name" in captured \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index f7fa9b0..a095186 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -36,12 +36,32 @@ def test_main_status(monkeypatch, capsys): def test_main_feed(monkeypatch): calls = {"fed": False} monkeypatch.setattr( - "study_pet.__main__.feed_pet", lambda: calls.update(fed=True) + "study_pet.__main__.feed_pet", lambda x=None: calls.update(fed=True) ) monkeypatch.setattr("sys.argv", ["prog", "feed"]) main() assert calls["fed"] +# feed option with parameter +def test_main_feed_with_parameter(monkeypatch): + calls = {"fed_with": None} + monkeypatch.setattr( + "study_pet.__main__.feed_pet", lambda x=None: calls.update(fed_with=x) + ) + monkeypatch.setattr("sys.argv", ["prog", "feed", "apple"]) + main() + assert calls["fed_with"] == "apple" + +# rename option with parameter +def test_main_rename_with_parameter(monkeypatch): + calls = {"renamed_with": None} + monkeypatch.setattr( + "study_pet.__main__.rename_pet", lambda x=None: calls.update(renamed_with=x) + ) + monkeypatch.setattr("sys.argv", ["prog", "rename", "NewPetName"]) + main() + assert calls["renamed_with"] == "NewPetName" + # unknown command def test_main_invalid_command(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["prog", "unknown"]) From 325597f8cc34bc8cc585b06b74ac1914d2b22f0d Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:15:03 -0500 Subject: [PATCH 26/40] added money for task completion --- README.md | 3 +- study_pet/__main__.py | 13 +++--- study_pet/data_manager.py | 3 +- study_pet/pet/__init__.py | 3 +- study_pet/pet/actions.py | 31 -------------- study_pet/tracker.py | 48 +++++++++++++++++++++- tests/test_actions.py | 19 +-------- tests/test_data_manager.py | 8 +++- tests/test_tracker.py | 82 ++++++++++++++++++++++++++++++++++++-- 9 files changed, 142 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 28b409c..c7b9fe5 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ ## Features - **📚 Study Session Tracking**: Start and end study sessions with automatic time tracking +- **✅ Task-Based Rewards**: Set tasks at the start of each session and earn 75 coins per completed task - **🐾 Virtual Pet System**: Your pet levels up based on total study hours (1 level per 5 hours) - **😊 Mood System**: Keep your pet happy by logging in regularly and feeding it -- **💰 Coin Collection**: Earn coins every 30 minutes to purchase food for your pet +- **💰 Earn Coins**: Complete tasks to earn coins that can be used to purchase food for your pet - **🍰 Feed Your Pet**: Choose from various foods (apple, cake, coffee, sushi, and more) to boost mood - **📈 Progress Tracking**: Monitor your study streaks, total study time, and pet status - **🔄 Auto-save**: Sessions automatically save on exit or interruption diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 97fc387..4866842 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -1,6 +1,6 @@ from . import start_session, end_session, get_status, reset_pet from .data_manager import load_state, save_state -from .pet import rename_pet, collect_money, feed_pet, check_daily_mood_decay +from .pet import rename_pet, feed_pet, check_daily_mood_decay import argparse import study_pet.tracker as tracker @@ -43,17 +43,14 @@ def actions_menu(): """Submenu for all pet-related actions.""" while True: print("\nActions Menu:") - print("1. Collect coins ") - print("2. Feed your pet") - print("3. Back") + print("1. Feed your pet") + print("2. Back") - choice = input("\nSelect an option (1–3): ").strip() + choice = input("\nSelect an option (1–2): ").strip() if choice == "1": - collect_money() - elif choice == "2": feed_pet() - elif choice == "3": + elif choice == "2": break else: print("Invalid option. Try again.") diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py index 7e77e55..88e7709 100644 --- a/study_pet/data_manager.py +++ b/study_pet/data_manager.py @@ -14,9 +14,10 @@ "mood": 100, "streak_days": 0, "money": 0, - "last_collect_time": None, "last_feed_date": None, "last_open_date": None, + "session_tasks_planned": 0, + "session_tasks_completed": 0, } diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py index e0a30cf..60dd244 100644 --- a/study_pet/pet/__init__.py +++ b/study_pet/pet/__init__.py @@ -1,10 +1,9 @@ from .core import get_status, check_daily_mood_decay -from .actions import rename_pet, collect_money, feed_pet +from .actions import rename_pet, feed_pet __all__ = [ "get_status", "rename_pet", - "collect_money", "feed_pet", "check_daily_mood_decay", ] diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 7ca9e3b..f1be1e4 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -29,37 +29,6 @@ def rename_pet(new_name: str = None): print(f"Pet name changed to '{new_name}'!\n") -def collect_money(): - """ - Allows user to collect money once every 30 minutes. - Grants a random reward between 50–100 coins. - """ - state = load_state() - now = time.time() - name = state.get("name", "Guido") - last_collect = state.get("last_collect_time", None) - - cooldown = 30 * 60 # 30 minutes in seconds - - if last_collect: - elapsed = now - last_collect - if elapsed < cooldown: - remaining = cooldown - elapsed - minutes = int(remaining // 60) - seconds = int(remaining % 60) - print(f"You can collect again in {minutes}m {seconds}s.") - return - - # reward logic - reward = random.randint(50, 100) - state["money"] = state.get("money", 0) + reward - state["last_collect_time"] = now - save_state(state) - - print(f"{name} found {reward} coins!") - print(f"Total balance: {state['money']} coins.") - - def feed_pet(food_name: str = None): """ Feed your pet with food purchased using money. diff --git a/study_pet/tracker.py b/study_pet/tracker.py index 98ca58d..f5955ae 100644 --- a/study_pet/tracker.py +++ b/study_pet/tracker.py @@ -26,15 +26,30 @@ def start_session(): print("A study session is already active.") return + # Ask for number of tasks to complete + while True: + try: + num_tasks = input("How many tasks do you plan to complete this session? ") + num_tasks = int(num_tasks) + if num_tasks < 0: + print("Please enter a positive number.") + continue + break + except ValueError: + print("Please enter a valid number.") + state["last_session_start"] = time.time() + state["session_tasks_planned"] = num_tasks + state["session_tasks_completed"] = 0 save_state(state) print(f"📘 Study session started at {datetime.now().strftime('%H:%M:%S')}") + print(f"📝 Tasks planned: {num_tasks}") def end_session(): """ Ends a study session and updates total study time. - Also triggers a pet level/exp update. + Also triggers a pet level/exp update and rewards coins for completed tasks. """ state = load_state() start_time = state.get("last_session_start", None) @@ -47,13 +62,42 @@ def end_session(): end_time = time.time() elapsed_hours = (end_time - start_time) / 3600 + # Ask for completed tasks + tasks_planned = state.get("session_tasks_planned", 0) + if tasks_planned > 0: + print(f"\n📝 You planned to complete {tasks_planned} task(s).") + while True: + try: + tasks_completed = input("How many tasks did you complete? ") + tasks_completed = int(tasks_completed) + if tasks_completed < 0: + print("Please enter a positive number.") + continue + if tasks_completed > tasks_planned: + confirm = input(f"You completed more than planned! Confirm {tasks_completed} tasks? (y/n) ").lower() + if confirm != 'y': + continue + break + except ValueError: + print("Please enter a valid number.") + + # Reward coins for completed tasks + coins_per_task = 75 + coins_earned = tasks_completed * coins_per_task + state["money"] = state.get("money", 0) + coins_earned + state["session_tasks_completed"] = tasks_completed + + print(f"✅ You completed {tasks_completed} task(s)!") + print(f"💰 Earned {coins_earned} coins! (Total: {state['money']} coins)") + # Update totals state["total_study_time"] += elapsed_hours state["last_session_start"] = None state["last_study_date"] = datetime.now().strftime("%Y-%m-%d") + state["session_tasks_planned"] = 0 save_state(state) - print(f"Study session ended. Duration: {elapsed_hours:.2f} hours") + print(f"\n⏱️ Study session ended. Duration: {elapsed_hours:.2f} hours") # Trigger pet update update_pet() diff --git a/tests/test_actions.py b/tests/test_actions.py index 032b613..a47cffd 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -3,7 +3,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from study_pet.pet.actions import rename_pet, collect_money, feed_pet +from study_pet.pet.actions import rename_pet, feed_pet from study_pet.data_manager import load_state, save_state, reset_state @@ -52,23 +52,6 @@ def test_rename_pet_with_parameter(capsys): assert "Pet name changed to 'NewName'!" in captured -# collect_money(): correct case -def test_collect_money_adds_balance(monkeypatch): - state = load_state() - start_money = state["money"] - collect_money() - state = load_state() - assert state["money"] > start_money - -# collect_money(): invalid case -def test_collect_money_respects_cooldown(monkeypatch, capsys): - state = load_state() - state["last_collect_time"] = time.time() - save_state(state) - collect_money() - captured = capsys.readouterr().out - assert "You can collect again" in captured - # feed_pet(): correct case def test_feed_pet_increases_mood(monkeypatch): state = load_state() diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py index 62665b5..dcc9b02 100644 --- a/tests/test_data_manager.py +++ b/tests/test_data_manager.py @@ -22,6 +22,10 @@ def test_load_state_creates_default_file(): assert os.path.exists(dm.DATA_PATH) assert "name" in state assert state["level"] == 1 + assert "session_tasks_planned" in state + assert "session_tasks_completed" in state + assert state["session_tasks_planned"] == 0 + assert state["session_tasks_completed"] == 0 def test_save_state_writes_to_file(): @@ -34,11 +38,13 @@ def test_save_state_writes_to_file(): def test_reset_state_resets_to_default(): - dm.save_state({"name": "WrongPet", "level": 10}) + dm.save_state({"name": "WrongPet", "level": 10, "session_tasks_planned": 5}) dm.reset_state() state = dm.load_state() assert state["name"] == "Guido" assert state["level"] == 1 + assert state["session_tasks_planned"] == 0 + assert state["session_tasks_completed"] == 0 def test_save_and_reload(): diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 90e1eeb..cfedcef 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -1,6 +1,7 @@ import sys, os import time import pytest +import builtins sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -23,19 +24,24 @@ def clean_data_file(): reset_state() -def test_start_session_creates_timestamp(): +def test_start_session_creates_timestamp(monkeypatch): + monkeypatch.setattr(builtins, "input", lambda _: "3") start_session() state = load_state() assert state["last_session_start"] is not None + assert state["session_tasks_planned"] == 3 -def test_end_session_updates_total_time(): +def test_end_session_updates_total_time(monkeypatch): + inputs = iter(["5", "3"]) # 5 tasks planned, 3 completed + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) start_session() time.sleep(0.5) end_session() state = load_state() assert state["total_study_time"] > 0 assert state["last_session_start"] is None + assert state["session_tasks_planned"] == 0 # reset after session ends def test_get_total_time_matches_state(): @@ -46,8 +52,8 @@ def test_get_total_time_matches_state(): assert abs(total_time - 12.34) < 0.001 -def test_reset_sessions_clears_active_session(): - +def test_reset_sessions_clears_active_session(monkeypatch): + monkeypatch.setattr(builtins, "input", lambda _: "2") start_session() reset_sessions() state = load_state() @@ -63,9 +69,77 @@ def test_end_session_without_start_does_not_crash(): def test_auto_end_session_respects_manual_close(monkeypatch): + monkeypatch.setattr(builtins, "input", lambda _: "1") start_session() monkeypatch.setattr("study_pet.tracker.manual_close", True) _auto_end_session() state = load_state() # still active because manual close skipped assert state["last_session_start"] is not None + + +def test_task_rewards_coins(monkeypatch): + """Test that completing tasks rewards coins correctly.""" + inputs = iter(["5", "3"]) # 5 tasks planned, 3 completed + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + + state = load_state() + initial_money = state["money"] + + start_session() + time.sleep(0.1) + end_session() + + state = load_state() + # 3 tasks * 75 coins per task = 225 coins + assert state["money"] == initial_money + 225 + assert state["session_tasks_completed"] == 3 + + +def test_no_tasks_planned_no_rewards(monkeypatch): + """Test that sessions with 0 tasks don't ask for completion or give rewards.""" + monkeypatch.setattr(builtins, "input", lambda _: "0") + + state = load_state() + initial_money = state["money"] + + start_session() + time.sleep(0.1) + end_session() + + state = load_state() + # No tasks, no rewards + assert state["money"] == initial_money + assert state["session_tasks_planned"] == 0 + + +def test_completing_more_tasks_than_planned(monkeypatch): + """Test completing more tasks than planned with confirmation.""" + inputs = iter(["3", "5", "y"]) # 3 planned, 5 completed, confirm yes + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + + state = load_state() + initial_money = state["money"] + + start_session() + time.sleep(0.1) + end_session() + + state = load_state() + # 5 tasks * 75 coins = 375 coins + assert state["money"] == initial_money + 375 + assert state["session_tasks_completed"] == 5 + + +def test_start_session_validates_task_input(monkeypatch, capsys): + """Test that start_session validates numeric input for tasks.""" + inputs = iter(["invalid", "-1", "3"]) # invalid, negative, then valid + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + + start_session() + + captured = capsys.readouterr().out + assert "Please enter a valid number" in captured or "Please enter a positive number" in captured + + state = load_state() + assert state["session_tasks_planned"] == 3 From 62c6000ec6779468262a4bda2f8ccd01d2f0e578 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:23:53 -0500 Subject: [PATCH 27/40] intial edits --- study_pet/__init__.py | 2 +- study_pet/__main__.py | 14 +++---- study_pet/pet/actions.py | 80 +++++++++++++++++++--------------------- study_pet/pet/core.py | 19 +++++----- 4 files changed, 56 insertions(+), 59 deletions(-) diff --git a/study_pet/__init__.py b/study_pet/__init__.py index 5689d45..d583678 100644 --- a/study_pet/__init__.py +++ b/study_pet/__init__.py @@ -8,4 +8,4 @@ def reset_pet(): """Resets all pet data and progress.""" _reset_state() - print("🐣 Your pet has been reborn! All progress reset.") + print("🥚 Your ball python has hatched anew! All progress reset. 🐍") diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 4866842..621ee7c 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -6,7 +6,7 @@ def main(): - parser = argparse.ArgumentParser(description="🐾 StudyPet CLI") + parser = argparse.ArgumentParser(description="� StudyPet") parser.add_argument( "command", nargs="?", @@ -42,8 +42,8 @@ def main(): def actions_menu(): """Submenu for all pet-related actions.""" while True: - print("\nActions Menu:") - print("1. Feed your pet") + print("\n🐍 Actions Menu:") + print("1. Feed your ball python") print("2. Back") choice = input("\nSelect an option (1–2): ").strip() @@ -59,9 +59,9 @@ def actions_menu(): def settings_menu(): """Submenu for settings and info.""" while True: - print("\nSettings Menu:") - print("1. Check pet status") - print("2. Rename your pet") + print("\n⚙️ Settings Menu:") + print("1. Check ball python status") + print("2. Rename your ball python") print("3. Reset all data") print("4. Back") @@ -86,7 +86,7 @@ def settings_menu(): def main_menu(): """Main entry menu.""" while True: - print("\n🐾 Welcome to StudyPet!\n") + print("\n� Welcome to SsstudyPet 🐍\n") print("1. Start studying ") print("2. End session") print("3. Actions") diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index f1be1e4..5123afa 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -10,15 +10,15 @@ def rename_pet(new_name: str = None): """ - Renames the pet and saves to state. + Renames the ball python and saves to state. If no new_name is passed, will ask user input interactively. """ state = load_state() old_name = state.get("name", "Unnamed") if not new_name: - print(f"\nCurrent name: {old_name}") - new_name = input("Enter new name for your pet: ").strip() + print(f"\n🐍 Current name: {old_name}") + new_name = input("Enter new name for your ball python: ").strip() if not new_name: print("Name cannot be empty.") @@ -26,40 +26,36 @@ def rename_pet(new_name: str = None): state["name"] = new_name save_state(state) - print(f"Pet name changed to '{new_name}'!\n") + print(f"🐍 Ball python name changed to '{new_name}'!\n") def feed_pet(food_name: str = None): """ - Feed your pet with food purchased using money. + Feed your ball python with food purchased using money. Each food has different cost and mood increase. Args: - food_name: Optional food name (apple, cake, coffee, carrot, sushi). + food_name: Optional food name (mouse, rat, quail, rabbit, cricket). If not provided, will show interactive menu. """ state = load_state() - name = state.get("name", "Guido") + name = state.get("name", "Monty") money = state.get("money", 0) mood = state.get("mood", 100) + # Ball python appropriate foods foods = { - "apple": {"cost": 80, "mood": 10, "emoji": "🍎", "msg": "Crunchy and sweet!"}, - "cake": { - "cost": 150, - "mood": 20, - "emoji": "🍰", - "msg": "So yummy! Sugar rush!", - }, - "coffee": {"cost": 50, "mood": 5, "emoji": "☕", "msg": "Ahh... more energy!"}, - "carrot": {"cost": 65, "mood": 8, "emoji": "🥕", "msg": "Healthy choice!"}, - "sushi": { - "cost": 130, + "mouse": {"cost": 50, "mood": 8, "emoji": "🐭", "msg": "Gulp! A tasty snack!"}, + "rat": { + "cost": 80, "mood": 15, - "emoji": "🍣", - "msg": "Delicious! I feel fancy!", + "emoji": "🐀", + "msg": "Mmm, satisfying meal!", }, - "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Yum! That was tasty!"}, + "quail": {"cost": 130, "mood": 20, "emoji": "🐦", "msg": "Exotic and delicious!"}, + "rabbit": {"cost": 150, "mood": 25, "emoji": "🐰", "msg": "What a feast!"}, + "cricket": {"cost": 30, "mood": 5, "emoji": "🦗", "msg": "A little crunchy treat!"}, + "custom": {"cost": 80, "mood": 10, "emoji": "🍽️", "msg": "Slither slither, nom nom!"}, } # If food_name is provided, use it directly @@ -87,31 +83,31 @@ def feed_pet(food_name: str = None): state["last_feed_date"] = datetime.now().strftime("%Y-%m-%d") save_state(state) - print(f"\n{food['emoji']} You fed {name} a {display_name}!") + print(f"\n{food['emoji']} You fed {name} the ball python a {display_name}!") print(food["msg"]) - print(f" Mood increased to {new_mood}/100.") - print(f" Remaining balance: {money} coins.\n") + print(f"🐍 Mood increased to {new_mood}/100.") + print(f"💰 Remaining balance: {money} coins.\n") return # Interactive menu mode (original behavior) - print(f"\n{name}'s current mood: {mood}/100 😊") - print(f"Current balance: {money} coins 💰") - print("Choose something to feed your pet:") - print("1. Apple 🍎 (Cost: 80 | +10 mood)") - print("2. Cake 🍰 (Cost: 150 | +20 mood)") - print("3. Coffee ☕ (Cost: 50 | +5 mood)") - print("4. Carrot 🥕 (Cost: 65 | +8 mood)") - print("5. Sushi 🍣 (Cost: 130 | +15 mood)") + print(f"\n🐍 {name}'s current mood: {mood}/100") + print(f"💰 Current balance: {money} coins") + print("Choose something to feed your ball python:") + print("1. Mouse 🐭 (Cost: 50 | +8 mood)") + print("2. Rat 🐀 (Cost: 80 | +15 mood)") + print("3. Quail 🐦 (Cost: 130 | +20 mood)") + print("4. Rabbit 🐰 (Cost: 150 | +25 mood)") + print("5. Cricket 🦗 (Cost: 30 | +5 mood)") print("6. Custom food ✏️ (Cost: 80 | +10 mood)") print("7. Return") choice = input("Select (1–7): ").strip() mapping = { - "1": "apple", - "2": "cake", - "3": "coffee", - "4": "carrot", - "5": "sushi", + "1": "mouse", + "2": "rat", + "3": "quail", + "4": "rabbit", + "5": "cricket", "6": "custom", } if choice == "7": @@ -125,8 +121,8 @@ def feed_pet(food_name: str = None): # handle custom name if selected == "custom": - custom_name = input("Enter your custom food name: ").strip() or "mystery meal" - food["msg"] = f"{name} happily ate your {custom_name}!" + custom_name = input("Enter your custom food name: ").strip() or "mystery prey" + food["msg"] = f"🐍 {name} slithered over and ate your {custom_name}!" display_name = custom_name else: display_name = selected @@ -144,7 +140,7 @@ def feed_pet(food_name: str = None): state["last_feed_date"] = datetime.now().strftime("%Y-%m-%d") save_state(state) - print(f"\n{food['emoji']} You fed {name} a {display_name}!") + print(f"\n{food['emoji']} You fed {name} the ball python a {display_name}!") print(food["msg"]) - print(f" Mood increased to {new_mood}/100.") - print(f" Remaining balance: {money} coins.\n") + print(f"🐍 Mood increased to {new_mood}/100.") + print(f"💰 Remaining balance: {money} coins.\n") diff --git a/study_pet/pet/core.py b/study_pet/pet/core.py index 639d2e0..5cdbd03 100644 --- a/study_pet/pet/core.py +++ b/study_pet/pet/core.py @@ -41,7 +41,7 @@ def update_pet(): new_level, new_exp = _calculate_level_exp(total_time) if new_level > prev_level: - print(f" {state['name']} leveled up! {prev_level} → {new_level}") + print(f"🐍 {state['name']} the ball python leveled up! {prev_level} → {new_level}") # Update state values state["level"] = new_level @@ -80,21 +80,22 @@ def get_status(): studying = "Studying now" if last_start else " Idle" + # Ball python mood descriptions if mood >= 80: - mood_status = "😊 Very happy!" + mood_status = "� Slithering happily!" elif mood >= 60: - mood_status = "😌 Content." + mood_status = "� Coiled and content." elif mood >= 40: - mood_status = "😕 A bit tired..." + mood_status = "� A bit sluggish..." elif mood >= 20: - mood_status = "😣 Needs care soon!" + mood_status = "� Needs feeding soon!" else: - mood_status = "😭 Very sad!" + mood_status = "� Very lethargic!" status = ( - f"\n Pet Status \n" + f"\n🐍 Ball Python Status 🐍\n" f"--------------------------------\n" - f"{name}\n" + f"{name} the Ball Python\n" f"Level: {level} ({exp:.0f} EXP)\n" f"Total Study Time: {total_time:.2f} hrs (+{elapsed:.2f}h current)\n" f"Last Study: {last_study}\n" @@ -179,7 +180,7 @@ def check_daily_mood_decay(): ) if new_mood == 0: - print("Your pet is very sad... please feed it soon!") + print("Your ball python is very lethargic... please feed it soon! 🐍") return new_mood From 80301b42050762ba98cb7eb26895d8e05db5c372 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:27:02 -0500 Subject: [PATCH 28/40] edits --- README.md | 14 +++++++------- study_pet/__main__.py | 4 ++-- study_pet/pet/core.py | 4 ++-- study_pet/tracker.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c7b9fe5..25a731d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ [![CI](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml/badge.svg?branch=pipfile-experiment)](https://github.com/swe-students-fall2025/3-python-package-team_cascade/actions/workflows/ci.yml) -# StudyPet +# 🐍 SsstudyPet -**StudyPet** is a Python package that provides an interactive command-line application to gamify your study sessions by letting you raise and care for a virtual pet. The more you study, the more your pet grows! Track your study time, earn coins, feed your pet, and watch it level up as you build consistent study habits. +**SsstudyPet** is a Python package that provides an interactive command-line application to gamify your study sessions by letting you raise and care for a virtual ball python. The more you study, the more your snake grows! Track your study time, earn coins, feed your python, and watch it level up as you build consistent study habits. ## Features - **📚 Study Session Tracking**: Start and end study sessions with automatic time tracking - **✅ Task-Based Rewards**: Set tasks at the start of each session and earn 75 coins per completed task -- **🐾 Virtual Pet System**: Your pet levels up based on total study hours (1 level per 5 hours) -- **😊 Mood System**: Keep your pet happy by logging in regularly and feeding it -- **💰 Earn Coins**: Complete tasks to earn coins that can be used to purchase food for your pet -- **🍰 Feed Your Pet**: Choose from various foods (apple, cake, coffee, sushi, and more) to boost mood -- **📈 Progress Tracking**: Monitor your study streaks, total study time, and pet status +- **� Virtual Ball Python**: Your snake levels up based on total study hours (1 level per 5 hours) +- **� Mood System**: Keep your python happy by logging in regularly and feeding it +- **💰 Earn Coins**: Complete tasks to earn coins that can be used to purchase food for your python +- **🐭 Feed Your Python**: Choose from various prey (mouse, rat, quail, rabbit, cricket) to boost mood +- **📈 Progress Tracking**: Monitor your study streaks, total study time, and python status - **🔄 Auto-save**: Sessions automatically save on exit or interruption - **🎮 Interactive Menu**: Easy-to-use CLI menu for all features diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 621ee7c..eda8889 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -6,7 +6,7 @@ def main(): - parser = argparse.ArgumentParser(description="� StudyPet") + parser = argparse.ArgumentParser(description="🐍 SsstudyPet") parser.add_argument( "command", nargs="?", @@ -92,7 +92,7 @@ def main_menu(): print("3. Actions") print("4. Settings") print("5. Close Menu (return to terminal)") - print("6. Exit (Close StudyPet to terminal)") + print("6. Exit (Close SsstudyPet to terminal)") choice = input("\nSelect an option (1–6): ").strip() if choice == "1": diff --git a/study_pet/pet/core.py b/study_pet/pet/core.py index 5cdbd03..8f1f020 100644 --- a/study_pet/pet/core.py +++ b/study_pet/pet/core.py @@ -1,9 +1,9 @@ """ study_pet/pet/core.py ------------------------------------------- -Core logic for StudyPet: +Core logic for SsstudyPet: Handles leveling, experience, and status updates -based on total study time. +based on total study time for your ball python. Design principles: - update_pet(): true update (writes to persistent JSON) diff --git a/study_pet/tracker.py b/study_pet/tracker.py index f5955ae..d05b573 100644 --- a/study_pet/tracker.py +++ b/study_pet/tracker.py @@ -1,10 +1,10 @@ """ -Tracks study sessions for StudyPet. +Tracks study sessions for SsstudyPet. Responsible for: - Starting and ending sessions - Updating total study time -- Triggering pet updates (level, exp) +- Triggering ball python updates (level, exp) """ import atexit @@ -101,7 +101,7 @@ def end_session(): # Trigger pet update update_pet() - print("Pet data updated!") + print("🐍 Ball python data updated!") def reset_sessions(): From 1fc7c2cbace7ea62b6eeebe10fa29f812e08717d Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:36:47 -0500 Subject: [PATCH 29/40] checked tests --- study_pet/__main__.py | 21 +++--- study_pet/data_manager.py | 1 + study_pet/pet/__init__.py | 3 +- study_pet/pet/actions.py | 118 ++++++++++++++++++++++++++++++++++ study_pet/pet/core.py | 12 ++-- tests/test_actions.py | 92 +++++++++++++++++++++++---- tests/test_core.py | 41 ++++++++++++ tests/test_data_manager.py | 5 +- tests/test_main.py | 24 ++++++- tests/test_morph.py | 127 +++++++++++++++++++++++++++++++++++++ 10 files changed, 416 insertions(+), 28 deletions(-) create mode 100644 tests/test_morph.py diff --git a/study_pet/__main__.py b/study_pet/__main__.py index eda8889..896df9c 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -1,6 +1,6 @@ from . import start_session, end_session, get_status, reset_pet from .data_manager import load_state, save_state -from .pet import rename_pet, feed_pet, check_daily_mood_decay +from .pet import rename_pet, feed_pet, set_morph, check_daily_mood_decay import argparse import study_pet.tracker as tracker @@ -11,13 +11,13 @@ def main(): "command", nargs="?", default="menu", - help="Available commands: start, end, status, feed, rename, menu", + help="Available commands: start, end, status, feed, rename, morph, menu", ) parser.add_argument( "arg", nargs="?", default=None, - help="Optional argument for feed (food name) or rename (new name)", + help="Optional argument for feed (food name), rename (new name), or morph (morph type)", ) args = parser.parse_args() @@ -33,10 +33,12 @@ def main(): feed_pet(args.arg) elif args.command == "rename": rename_pet(args.arg) + elif args.command == "morph": + set_morph(args.arg) elif args.command == "menu": main_menu() else: - print("Unknown command. Use: start | end | status | feed [food] | rename [name] | menu") + print("Unknown command. Use: start | end | status | feed [food] | rename [name] | morph [type] | menu") def actions_menu(): @@ -62,22 +64,25 @@ def settings_menu(): print("\n⚙️ Settings Menu:") print("1. Check ball python status") print("2. Rename your ball python") - print("3. Reset all data") - print("4. Back") + print("3. Change ball python morph") + print("4. Reset all data") + print("5. Back") - choice = input("\nSelect an option (1–4): ").strip() + choice = input("\nSelect an option (1–5): ").strip() if choice == "1": print(get_status()) elif choice == "2": rename_pet() elif choice == "3": + set_morph() + elif choice == "4": confirm = input( "This will reset all progress. Type 'byebye' to confirm " ).lower() if confirm == "byebye": reset_pet() - elif choice == "4": + elif choice == "5": break else: print("Invalid option. Try again.") diff --git a/study_pet/data_manager.py b/study_pet/data_manager.py index 88e7709..91dbc7c 100644 --- a/study_pet/data_manager.py +++ b/study_pet/data_manager.py @@ -18,6 +18,7 @@ "last_open_date": None, "session_tasks_planned": 0, "session_tasks_completed": 0, + "morph": "Normal/Wild Type", } diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py index 60dd244..c5afe8c 100644 --- a/study_pet/pet/__init__.py +++ b/study_pet/pet/__init__.py @@ -1,9 +1,10 @@ from .core import get_status, check_daily_mood_decay -from .actions import rename_pet, feed_pet +from .actions import rename_pet, feed_pet, set_morph __all__ = [ "get_status", "rename_pet", "feed_pet", + "set_morph", "check_daily_mood_decay", ] diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 5123afa..703a718 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -144,3 +144,121 @@ def feed_pet(food_name: str = None): print(food["msg"]) print(f"🐍 Mood increased to {new_mood}/100.") print(f"💰 Remaining balance: {money} coins.\n") + + +def set_morph(morph_name: str = None): + """ + Set the ball python's morph (color pattern). + If no morph_name is passed, will show interactive menu. + """ + state = load_state() + current_morph = state.get("morph", "Normal/Wild Type") + + # Available morphs with descriptions + morphs = { + "1": { + "name": "Normal/Wild Type", + "desc": "Natural coloration with brown and tan patterns" + }, + "2": { + "name": "Albino", + "desc": "Yellow, orange, and white with red/pink eyes" + }, + "3": { + "name": "Pastel", + "desc": "Brightened colors with lighter yellows" + }, + "4": { + "name": "Spider", + "desc": "Bold, high-contrast web-like appearance" + }, + "5": { + "name": "Mojave", + "desc": "Lighter sides with prominent dorsal stripe" + }, + "6": { + "name": "Pinstripe", + "desc": "Clean, thin stripe down the spine" + }, + "7": { + "name": "Clown", + "desc": "Distinctive head pattern and elongated blotches" + }, + "8": { + "name": "Banana", + "desc": "Bright yellow and purple/lavender coloring" + }, + "9": { + "name": "Black Pastel", + "desc": "Darkened coloration with good contrast" + }, + "10": { + "name": "Cinnamon", + "desc": "Rich brown and caramel tones" + }, + "11": { + "name": "Custom", + "desc": "Create your own unique morph!" + } + } + + # If morph_name is provided directly + if morph_name: + morph_name = morph_name.strip() + # Check if it matches any morph name + found = False + for morph_data in morphs.values(): + if morph_data["name"].lower() == morph_name.lower(): + state["morph"] = morph_data["name"] + save_state(state) + print(f"🐍 Your ball python's morph is now: {morph_data['name']}!") + print(f" {morph_data['desc']}") + found = True + break + + if not found: + # Allow custom morph names from command line + state["morph"] = morph_name + save_state(state) + print(f"🐍 Your ball python's morph is now: {morph_name} (custom)!") + return + + # Interactive menu mode + print(f"\n🐍 Current morph: {current_morph}") + print("\n✨ Choose your ball python's morph:\n") + + for key, morph_data in morphs.items(): + print(f"{key:>2}. {morph_data['name']:<20} - {morph_data['desc']}") + + print(f"\n{len(morphs) + 1}. Cancel") + + choice = input(f"\nSelect (1–{len(morphs) + 1}): ").strip() + + if choice == str(len(morphs) + 1): + print("Morph selection cancelled.") + return + + if choice not in morphs: + print("❌ Invalid choice.") + return + + # Handle custom morph option + if choice == "11": + custom_morph = input("\n✨ Enter your custom morph name: ").strip() + if not custom_morph: + print("❌ Morph name cannot be empty.") + return + + state["morph"] = custom_morph + save_state(state) + print(f"\n🐍 Your ball python's morph is now: {custom_morph} (custom)!") + print("✨ Your python looks unique and beautiful!\n") + return + + selected_morph = morphs[choice]["name"] + state["morph"] = selected_morph + save_state(state) + + print(f"\n✨ Your ball python's morph is now: {selected_morph}!") + print(f" {morphs[choice]['desc']}") + print("🐍 Your python looks beautiful!\n") diff --git a/study_pet/pet/core.py b/study_pet/pet/core.py index 8f1f020..4ceca22 100644 --- a/study_pet/pet/core.py +++ b/study_pet/pet/core.py @@ -74,6 +74,7 @@ def get_status(): name = state.get("name", "Unnamed") mood = state.get("mood", 100) money = state.get("money", 0) + morph = state.get("morph", "Normal/Wild Type") streak = state.get("streak_days", 0) last_study = state.get("last_study_date", "N/A") @@ -82,20 +83,21 @@ def get_status(): # Ball python mood descriptions if mood >= 80: - mood_status = "� Slithering happily!" + mood_status = "Slithering happily!" elif mood >= 60: - mood_status = "� Coiled and content." + mood_status = "Coiled and content." elif mood >= 40: - mood_status = "� A bit sluggish..." + mood_status = "A bit sluggish..." elif mood >= 20: - mood_status = "� Needs feeding soon!" + mood_status = "Needs feeding soon!" else: - mood_status = "� Very lethargic!" + mood_status = "Very lethargic!" status = ( f"\n🐍 Ball Python Status 🐍\n" f"--------------------------------\n" f"{name} the Ball Python\n" + f"Morph: {morph}\n" f"Level: {level} ({exp:.0f} EXP)\n" f"Total Study Time: {total_time:.2f} hrs (+{elapsed:.2f}h current)\n" f"Last Study: {last_study}\n" diff --git a/tests/test_actions.py b/tests/test_actions.py index a47cffd..5879dae 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -59,7 +59,7 @@ def test_feed_pet_increases_mood(monkeypatch): state["mood"] = 60 save_state(state) - inputs = iter(["1"]) # Apple + inputs = iter(["1"]) # Mouse monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) feed_pet() new_state = load_state() @@ -105,7 +105,7 @@ def test_feed_pet_custom_food(monkeypatch): # feed_pet(): menu item 6 - custom food empty def test_feed_pet_custom_food_empty_name(monkeypatch, capsys): - # if user input is empty, mystery meal + # if user input is empty, mystery prey state = load_state() state["money"] = 500 save_state(state) @@ -115,7 +115,7 @@ def test_feed_pet_custom_food_empty_name(monkeypatch, capsys): feed_pet() captured = capsys.readouterr().out - assert "mystery meal" in captured + assert "mystery prey" in captured # feed_pet(): menu item 7 - return def test_feed_pet_return(monkeypatch): @@ -132,21 +132,21 @@ def test_feed_pet_return(monkeypatch): assert new_state["money"] == 500 assert new_state["mood"] == 50 -# feed_pet(): with parameter - apple -def test_feed_pet_with_parameter_apple(capsys): +# feed_pet(): with parameter - mouse (ball python food) +def test_feed_pet_with_parameter_mouse(capsys): state = load_state() state["money"] = 500 state["mood"] = 60 save_state(state) - feed_pet("apple") + feed_pet("mouse") new_state = load_state() captured = capsys.readouterr().out - assert new_state["mood"] == 70 # 60 + 10 - assert new_state["money"] == 420 # 500 - 80 - assert "apple" in captured.lower() - assert "🍎" in captured + assert new_state["mood"] == 68 # 60 + 8 + assert new_state["money"] == 450 # 500 - 50 + assert "mouse" in captured.lower() + assert "🐭" in captured # feed_pet(): with parameter - invalid food name def test_feed_pet_invalid_food_name(capsys): @@ -161,4 +161,74 @@ def test_feed_pet_invalid_food_name(capsys): captured = capsys.readouterr().out assert new_state["mood"] == 60 # unchanged assert new_state["money"] == 500 # unchanged - assert "Invalid food name" in captured \ No newline at end of file + assert "Invalid food name" in captured + + +# feed_pet(): test all ball python foods +def test_feed_pet_rat(capsys): + """Test feeding rat to ball python""" + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("rat") + + new_state = load_state() + assert new_state["mood"] == 75 # 60 + 15 + assert new_state["money"] == 420 # 500 - 80 + + +def test_feed_pet_cricket(capsys): + """Test feeding cricket to ball python""" + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("cricket") + + new_state = load_state() + assert new_state["mood"] == 65 # 60 + 5 + assert new_state["money"] == 470 # 500 - 30 + + +def test_feed_pet_quail(capsys): + """Test feeding quail to ball python""" + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("quail") + + new_state = load_state() + assert new_state["mood"] == 80 # 60 + 20 + assert new_state["money"] == 370 # 500 - 130 + + +def test_feed_pet_rabbit(capsys): + """Test feeding rabbit to ball python""" + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("rabbit") + + new_state = load_state() + assert new_state["mood"] == 85 # 60 + 25 + assert new_state["money"] == 350 # 500 - 150 + + +def test_ball_python_theme_in_output(capsys): + """Test that ball python theme appears in feed output""" + state = load_state() + state["money"] = 500 + state["mood"] = 60 + save_state(state) + + feed_pet("mouse") + + captured = capsys.readouterr().out + assert "ball python" in captured.lower() \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index 55702b2..9e61049 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -50,3 +50,44 @@ def test_check_daily_mood_decay_reduces_mood(): save_state(s) new_mood = check_daily_mood_decay() assert new_mood < 80 + + +def test_get_status_shows_ball_python_theme(): + """Test that status display includes ball python theme""" + status = get_status() + assert "Ball Python" in status + assert "🐍" in status + + +def test_get_status_shows_morph(): + """Test that status display includes morph information""" + state = load_state() + state["morph"] = "Albino" + save_state(state) + + status = get_status() + assert "Morph:" in status + assert "Albino" in status + + +def test_get_status_shows_default_morph(): + """Test that status displays default morph""" + status = get_status() + assert "Normal/Wild Type" in status or "Morph:" in status + + +def test_ball_python_mood_descriptions(): + """Test that ball python mood descriptions are used""" + state = load_state() + + # Test high mood (slithering happily) + state["mood"] = 90 + save_state(state) + status = get_status() + assert "Slithering happily" in status or "🐍" in status + + # Test low mood (lethargic) + state["mood"] = 10 + save_state(state) + status = get_status() + assert "lethargic" in status.lower() or "🐍" in status diff --git a/tests/test_data_manager.py b/tests/test_data_manager.py index dcc9b02..be137f6 100644 --- a/tests/test_data_manager.py +++ b/tests/test_data_manager.py @@ -24,8 +24,10 @@ def test_load_state_creates_default_file(): assert state["level"] == 1 assert "session_tasks_planned" in state assert "session_tasks_completed" in state + assert "morph" in state assert state["session_tasks_planned"] == 0 assert state["session_tasks_completed"] == 0 + assert state["morph"] == "Normal/Wild Type" def test_save_state_writes_to_file(): @@ -38,13 +40,14 @@ def test_save_state_writes_to_file(): def test_reset_state_resets_to_default(): - dm.save_state({"name": "WrongPet", "level": 10, "session_tasks_planned": 5}) + dm.save_state({"name": "WrongPet", "level": 10, "session_tasks_planned": 5, "morph": "Custom"}) dm.reset_state() state = dm.load_state() assert state["name"] == "Guido" assert state["level"] == 1 assert state["session_tasks_planned"] == 0 assert state["session_tasks_completed"] == 0 + assert state["morph"] == "Normal/Wild Type" def test_save_and_reload(): diff --git a/tests/test_main.py b/tests/test_main.py index a095186..08be1ce 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -48,9 +48,9 @@ def test_main_feed_with_parameter(monkeypatch): monkeypatch.setattr( "study_pet.__main__.feed_pet", lambda x=None: calls.update(fed_with=x) ) - monkeypatch.setattr("sys.argv", ["prog", "feed", "apple"]) + monkeypatch.setattr("sys.argv", ["prog", "feed", "mouse"]) main() - assert calls["fed_with"] == "apple" + assert calls["fed_with"] == "mouse" # rename option with parameter def test_main_rename_with_parameter(monkeypatch): @@ -62,6 +62,26 @@ def test_main_rename_with_parameter(monkeypatch): main() assert calls["renamed_with"] == "NewPetName" +# morph option +def test_main_morph(monkeypatch): + calls = {"morph_set": False} + monkeypatch.setattr( + "study_pet.__main__.set_morph", lambda x=None: calls.update(morph_set=True) + ) + monkeypatch.setattr("sys.argv", ["prog", "morph"]) + main() + assert calls["morph_set"] + +# morph option with parameter +def test_main_morph_with_parameter(monkeypatch): + calls = {"morph_with": None} + monkeypatch.setattr( + "study_pet.__main__.set_morph", lambda x=None: calls.update(morph_with=x) + ) + monkeypatch.setattr("sys.argv", ["prog", "morph", "Albino"]) + main() + assert calls["morph_with"] == "Albino" + # unknown command def test_main_invalid_command(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["prog", "unknown"]) diff --git a/tests/test_morph.py b/tests/test_morph.py new file mode 100644 index 0000000..8f5d7a1 --- /dev/null +++ b/tests/test_morph.py @@ -0,0 +1,127 @@ +import sys, os, pytest, builtins + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from study_pet.pet.actions import set_morph +from study_pet.data_manager import load_state, save_state, reset_state + + +@pytest.fixture(autouse=True) +def clean_state(): + reset_state() + yield + reset_state() + + +def test_set_morph_default(): + """Test that default morph is Normal/Wild Type""" + state = load_state() + assert state["morph"] == "Normal/Wild Type" + + +def test_set_morph_with_parameter(capsys): + """Test setting morph with parameter""" + set_morph("Albino") + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Albino" + assert "Albino" in captured + + +def test_set_morph_interactive(monkeypatch, capsys): + """Test setting morph through interactive menu""" + monkeypatch.setattr(builtins, "input", lambda _: "5") # Choose Mojave + set_morph() + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Mojave" + assert "Mojave" in captured + + +def test_set_morph_invalid_parameter(capsys): + """Test setting morph with invalid parameter""" + set_morph("InvalidMorph") + state = load_state() + captured = capsys.readouterr().out + + # Should remain default + assert state["morph"] == "Normal/Wild Type" + assert "Invalid morph name" in captured + + +def test_set_morph_cancel(monkeypatch, capsys): + """Test canceling morph selection""" + monkeypatch.setattr(builtins, "input", lambda _: "11") # Choose cancel option + set_morph() + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Normal/Wild Type" + assert "cancelled" in captured.lower() + + +def test_set_morph_case_insensitive(capsys): + """Test that morph names are case insensitive""" + set_morph("pAsTel") + state = load_state() + + assert state["morph"] == "Pastel" + + +def test_all_morphs_selectable(monkeypatch): + """Test that all 10 morphs can be selected""" + morphs = [ + ("1", "Normal/Wild Type"), + ("2", "Albino"), + ("3", "Pastel"), + ("4", "Spider"), + ("5", "Mojave"), + ("6", "Pinstripe"), + ("7", "Clown"), + ("8", "Banana"), + ("9", "Black Pastel"), + ("10", "Cinnamon") + ] + + for choice, expected_morph in morphs: + reset_state() + monkeypatch.setattr(builtins, "input", lambda _, c=choice: c) + set_morph() + state = load_state() + assert state["morph"] == expected_morph, f"Failed to set {expected_morph}" + + +def test_custom_morph_interactive(monkeypatch, capsys): + """Test creating a custom morph through interactive menu""" + inputs = iter(["11", "Blue Dream"]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + set_morph() + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Blue Dream" + assert "custom" in captured.lower() + + +def test_custom_morph_empty_name(monkeypatch, capsys): + """Test that empty custom morph name is rejected""" + inputs = iter(["11", ""]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + set_morph() + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Normal/Wild Type" # Should remain default + assert "cannot be empty" in captured.lower() + + +def test_custom_morph_with_parameter(capsys): + """Test setting custom morph with parameter""" + set_morph("Super Pastel Mojave") + state = load_state() + captured = capsys.readouterr().out + + assert state["morph"] == "Super Pastel Mojave" + assert "custom" in captured.lower() From 82f4a791631efff5717c9397b6ff6297298e7557 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:48:42 -0500 Subject: [PATCH 30/40] edit assertion in test --- tests/test_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index 5879dae..b09d450 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -49,7 +49,7 @@ def test_rename_pet_with_parameter(capsys): new_state = load_state() captured = capsys.readouterr().out assert new_state["name"] == "NewName" - assert "Pet name changed to 'NewName'!" in captured + assert "🐍 Ball python name changed to 'NewName'!" in captured # feed_pet(): correct case From 0b68ee8971c8b91bc53a062dd98cd83248fc6b5e Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:51:56 -0500 Subject: [PATCH 31/40] edit test --- study_pet/pet/actions.py | 6 ++---- tests/test_morph.py | 9 +++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 703a718..c95116c 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -217,10 +217,8 @@ def set_morph(morph_name: str = None): break if not found: - # Allow custom morph names from command line - state["morph"] = morph_name - save_state(state) - print(f"🐍 Your ball python's morph is now: {morph_name} (custom)!") + print(f"Invalid morph name: {morph_name}") + print("Available morphs: " + ", ".join([m["name"] for m in morphs.values() if m["name"] != "Custom"])) return # Interactive menu mode diff --git a/tests/test_morph.py b/tests/test_morph.py index 8f5d7a1..d30b92d 100644 --- a/tests/test_morph.py +++ b/tests/test_morph.py @@ -117,11 +117,12 @@ def test_custom_morph_empty_name(monkeypatch, capsys): assert "cannot be empty" in captured.lower() -def test_custom_morph_with_parameter(capsys): - """Test setting custom morph with parameter""" +def test_custom_morph_with_parameter_invalid(capsys): + """Test that invalid morph names via parameter don't update state""" set_morph("Super Pastel Mojave") state = load_state() captured = capsys.readouterr().out - assert state["morph"] == "Super Pastel Mojave" - assert "custom" in captured.lower() + # Should remain default since it's not a preset morph + assert state["morph"] == "Normal/Wild Type" + assert "Invalid morph name" in captured From 2140f56bbb545d773f95ed9ceee2774bdc08c4d5 Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 15:55:30 -0500 Subject: [PATCH 32/40] edit tests --- tests/test_morph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_morph.py b/tests/test_morph.py index d30b92d..57fa91f 100644 --- a/tests/test_morph.py +++ b/tests/test_morph.py @@ -53,7 +53,7 @@ def test_set_morph_invalid_parameter(capsys): def test_set_morph_cancel(monkeypatch, capsys): """Test canceling morph selection""" - monkeypatch.setattr(builtins, "input", lambda _: "11") # Choose cancel option + monkeypatch.setattr(builtins, "input", lambda _: "12") # Choose cancel option (len(morphs) + 1) set_morph() state = load_state() captured = capsys.readouterr().out From a140c391369bfb7ddc080ebcfd90347ca6cfdbdc Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 17:14:58 -0500 Subject: [PATCH 33/40] added additions to readme + build instructions --- .github/workflows/ci.yml | 4 + README.md | 244 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 42 +++++++ 3 files changed, 290 insertions(+) create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877e542..326546f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,7 @@ jobs: - name: Run tests run: | pipenv run pytest --maxfail=1 --disable-warnings -q + + - name: Build package + run: | + pipenv run python -m build diff --git a/README.md b/README.md index 25a731d..d159527 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,247 @@ git clone https://github.com/swe-students-fall2025/3-python-package-team_cascade cd team_cascade pipenv install --dev ``` + +## Using SsstudyPet in Your Code + +You can import and use SsstudyPet's functions in your own Python programs. Here's documentation for all available functions: + +### Study Session Management + +```python +from study_pet.tracker import start_session, end_session + +# Start a study session with task planning +start_session() +# Prompts: "How many tasks do you plan to complete this session?" +# Sets session_tasks_planned and records start time + +# End a study session with task completion +end_session() +# Prompts: "How many tasks did you complete this session?" +# Rewards 75 coins per completed task +# Updates total study time and saves progress +``` + +### Pet Care Functions + +```python +from study_pet.pet import feed_pet, rename_pet, set_morph + +# Feed your ball python (costs coins, boosts mood) +feed_pet("mouse") # Costs 50 coins, +10 mood +feed_pet("rat") # Costs 80 coins, +15 mood +feed_pet("quail") # Costs 130 coins, +25 mood +feed_pet("rabbit") # Costs 150 coins, +30 mood +feed_pet("cricket") # Costs 30 coins, +5 mood + +# Rename your ball python +rename_pet("Slithers") + +# Set your ball python's morph (color pattern) +set_morph("Banana") # Choose from 10 preset morphs +set_morph("Custom Morph Name") # Or create your own custom morph +# Available presets: Banana, Pastel, Pied, Clown, Mojave, +# Cinnamon, Albino, Blue Eyed Leucistic, GHI, Spider +``` + +### Status and Data Functions + +```python +from study_pet.pet import get_status +from study_pet.data_manager import load_state, save_state, reset_data + +# Get your ball python's current status +status = get_status() +print(status) # Returns formatted status string with all pet info + +# Load current saved data +state = load_state() +print(f"Level: {state['level']}") +print(f"Coins: {state['coins']}") +print(f"Morph: {state['morph']}") + +# Manually save data (normally done automatically) +save_state(state) + +# Reset all data (use with caution!) +reset_data() +``` + +### Example Program + +See our complete example program that demonstrates all functions: [example.py](https://github.com/swe-students-fall2025/3-python-package-team_cascade/blob/main/example.py) + +```python +# example.py - Complete demonstration of SsstudyPet +from study_pet.tracker import start_session, end_session +from study_pet.pet import feed_pet, rename_pet, set_morph, get_status +from study_pet.data_manager import load_state + +# Start a study session +print("Starting study session...") +start_session() # Will prompt for task count + +# Do some studying... (simulated) +print("\n📚 Studying...\n") + +# End the session and earn rewards +end_session() # Will prompt for completed tasks + +# Check your ball python's status +print("\n" + get_status()) + +# Name your python +rename_pet("Monty") +print("\n🐍 Your ball python is now named Monty!") + +# Set a morph +set_morph("Banana") +print("\n✨ Your python is now a beautiful Banana morph!") + +# Feed your python +feed_pet("mouse") +print("\n🐭 Fed your python a mouse!") + +# Check final status +print("\n" + get_status()) + +# Access raw data +state = load_state() +print(f"\n💰 Total coins: {state['coins']}") +print(f"⏱️ Total study time: {state['total_minutes']} minutes") +``` + +## Contributing to SsstudyPet + +Want to help make SsstudyPet better? Here's how to set up your development environment: + +### Prerequisites + +- Python 3.10 or higher (Python 3.13 recommended) +- pipenv for dependency management + +### Setup Instructions + +1. **Clone the repository** + ```bash + git clone https://github.com/swe-students-fall2025/3-python-package-team_cascade.git + cd 3-python-package-team_cascade-4 + ``` + +2. **Install pipenv** (if not already installed) + ```bash + pip install pipenv + ``` + +3. **Set up the virtual environment and install dependencies** + ```bash + # Install all dependencies including dev dependencies + pipenv install --dev + + # This installs: + # - pytest (for testing) + # - build (for building the package) + # - twine (for publishing to PyPI) + ``` + +4. **Activate the virtual environment** + ```bash + pipenv shell + ``` + +5. **Run the tests** + ```bash + # Run all tests + pytest tests/ -v + + # Run tests with coverage + pytest tests/ --cov=study_pet --cov-report=html + + # Run specific test file + pytest tests/test_actions.py -v + ``` + +6. **Build the package** + ```bash + # Build distribution files + python -m build + + # This creates dist/ directory with: + # - .tar.gz (source distribution) + # - .whl (wheel distribution) + ``` + +7. **Test the package locally** + ```bash + # Install in editable mode for development + pip install -e . + + # Run the CLI + study-pet menu + ``` + +### Development Workflow + +1. Create a new feature branch: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and test thoroughly: + ```bash + pytest tests/ -v + ``` + +3. Commit your changes: + ```bash + git add . + git commit -m "Add your descriptive commit message" + ``` + +4. Push to GitHub and create a pull request: + ```bash + git push origin feature/your-feature-name + ``` + +5. Wait for CI tests to pass (GitHub Actions automatically runs tests) + +### Project Structure + +``` +study_pet/ +├── __init__.py # Package initialization +├── __main__.py # CLI entry point +├── data_manager.py # Data persistence functions +├── tracker.py # Study session tracking +└── pet/ + ├── __init__.py # Pet module exports + ├── core.py # Pet status and leveling + └── actions.py # Pet interaction functions +tests/ +├── test_actions.py # Tests for pet actions +├── test_core.py # Tests for pet core functions +├── test_data_manager.py # Tests for data management +├── test_tracker.py # Tests for session tracking +├── test_main.py # Tests for CLI commands +└── test_morph.py # Tests for morph system +``` + +### Running Specific Tests + +```bash +# Test specific functionality +pytest tests/test_morph.py::test_set_morph_valid -v +pytest tests/test_tracker.py -v +pytest tests/test_actions.py::test_feed_pet -v + +# Test with output +pytest tests/ -v -s + +# Generate coverage report +pytest tests/ --cov=study_pet --cov-report=term-missing +``` + +## PyPI Package + +This package is available on PyPI: [study-pet](https://pypi.org/project/study-pet/) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3226b9f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "study-pet" +version = "0.1.0" +description = "A Python package to gamify your study sessions by raising a virtual ball python" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "Catherine Yu", email = "catherine@example.com"}, + {name = "Leo Fu", email = "leo@example.com"}, + {name = "JunHao Chen", email = "junhao@example.com"}, + {name = "Majo Salgado", email = "majo@example.com"}, + {name = "Zeba Shafi", email = "zeba@example.com"}, +] +keywords = ["study", "productivity", "gamification", "pet", "cli", "ball-python"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Education", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Education", +] + +[project.urls] +Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_cascade" +Repository = "https://github.com/swe-students-fall2025/3-python-package-team_cascade" +Issues = "https://github.com/swe-students-fall2025/3-python-package-team_cascade/issues" + +[project.scripts] +study-pet = "study_pet.__main__:main" + +[tool.setuptools.packages.find] +include = ["study_pet*"] +exclude = ["tests*"] From 0acf25782d5c94cc772b9c5b69479ccfe5d6c9bd Mon Sep 17 00:00:00 2001 From: Zeba-Shafi Date: Mon, 3 Nov 2025 17:22:01 -0500 Subject: [PATCH 34/40] added example.py --- example.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 example.py diff --git a/example.py b/example.py new file mode 100644 index 0000000..126df57 --- /dev/null +++ b/example.py @@ -0,0 +1,118 @@ +""" +example.py - Complete demonstration of SsstudyPet + +This example program demonstrates all the functions available in the +SsstudyPet package. It shows how to: +- Start and end study sessions with task tracking +- Feed your ball python +- Rename your ball python +- Set morphs (color patterns) +- Check status and access saved data +""" + +from study_pet.tracker import start_session, end_session +from study_pet.pet import feed_pet, rename_pet, set_morph, get_status +from study_pet.data_manager import load_state + + +def main(): + print("=" * 60) + print("🐍 Welcome to SsstudyPet Example Program 🐍") + print("=" * 60) + print("\nThis program demonstrates all SsstudyPet functions.\n") + + # Start a study session + print("📚 STARTING A STUDY SESSION") + print("-" * 60) + start_session() # Will prompt for task count + + print("\n✏️ Imagine you're studying now...") + print(" (In real use, you'd close the terminal and study!)") + input("\n Press Enter to end your study session...") + + # End the session and earn rewards + print("\n🎉 ENDING STUDY SESSION") + print("-" * 60) + end_session() # Will prompt for completed tasks + + # Check your ball python's status + print("\n📊 CHECKING YOUR BALL PYTHON'S STATUS") + print("-" * 60) + print(get_status()) + + # Rename your python + print("\n✏️ RENAMING YOUR BALL PYTHON") + print("-" * 60) + new_name = input("What would you like to name your ball python? (or press Enter for 'Monty'): ").strip() + if not new_name: + new_name = "Monty" + rename_pet(new_name) + print(f"\n🐍 Your ball python is now named {new_name}!") + + # Set a morph (color pattern) + print("\n🎨 SETTING A MORPH (COLOR PATTERN)") + print("-" * 60) + print("Available morphs:") + print(" 1. Banana 2. Pastel 3. Pied 4. Clown") + print(" 5. Mojave 6. Cinnamon 7. Albino 8. Blue Eyed Leucistic") + print(" 9. GHI 10. Spider") + + morph_choice = input("\nEnter a morph name (or press Enter for 'Banana'): ").strip() + if not morph_choice: + morph_choice = "Banana" + set_morph(morph_choice) + print(f"\n✨ Your python is now a beautiful {morph_choice} morph!") + + # Check status again to see the changes + print("\n📊 UPDATED STATUS") + print("-" * 60) + print(get_status()) + + # Feed your python + print("\n🍽️ FEEDING YOUR BALL PYTHON") + print("-" * 60) + print("Available prey:") + print(" - cricket (30 coins, +5 mood)") + print(" - mouse (50 coins, +10 mood)") + print(" - rat (80 coins, +15 mood)") + print(" - quail (130 coins, +25 mood)") + print(" - rabbit (150 coins, +30 mood)") + + # Check current coins + state = load_state() + print(f"\nYou currently have {state['coins']} coins.") + + prey_choice = input("\nWhat would you like to feed your python? (or press Enter for 'mouse'): ").strip().lower() + if not prey_choice: + prey_choice = "mouse" + + feed_pet(prey_choice) + print(f"\n🐭 Fed your python a {prey_choice}!") + + # Show final status + print("\n📊 FINAL STATUS") + print("-" * 60) + print(get_status()) + + # Access and display raw data + print("\n📈 RAW DATA") + print("-" * 60) + state = load_state() + print(f"💰 Total coins: {state['coins']}") + print(f"⏱️ Total study time: {state['total_minutes']} minutes") + print(f"📅 Current streak: {state['streak']} days") + print(f"🎯 Level: {state['level']}") + print(f"😊 Mood: {state['mood']}") + print(f"🎨 Morph: {state['morph']}") + print(f"📝 Session tasks planned: {state['session_tasks_planned']}") + print(f"✅ Session tasks completed: {state['session_tasks_completed']}") + + print("\n" + "=" * 60) + print("🎉 Example program complete!") + print("=" * 60) + print("\nTo use SsstudyPet normally, run: study-pet menu") + print("or use individual commands like: study-pet start, study-pet status, etc.") + + +if __name__ == "__main__": + main() From f897ee7eff1db76a355755522db6468ae4cfb81d Mon Sep 17 00:00:00 2001 From: catherineyu2014 Date: Tue, 4 Nov 2025 11:09:57 -0500 Subject: [PATCH 35/40] updated dictonary key errors --- example.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/example.py b/example.py index 126df57..373fc96 100644 --- a/example.py +++ b/example.py @@ -80,7 +80,7 @@ def main(): # Check current coins state = load_state() - print(f"\nYou currently have {state['coins']} coins.") + print(f"\nYou currently have {state.get('coins', 0)} coins.") prey_choice = input("\nWhat would you like to feed your python? (or press Enter for 'mouse'): ").strip().lower() if not prey_choice: @@ -98,12 +98,12 @@ def main(): print("\n📈 RAW DATA") print("-" * 60) state = load_state() - print(f"💰 Total coins: {state['coins']}") - print(f"⏱️ Total study time: {state['total_minutes']} minutes") - print(f"📅 Current streak: {state['streak']} days") - print(f"🎯 Level: {state['level']}") - print(f"😊 Mood: {state['mood']}") - print(f"🎨 Morph: {state['morph']}") + print(f"💰 Total coins: {state.get('coins', 0)}") + print(f"⏱️ Total study time: {state.get('total_minutes', 0)} minutes") + print(f"📅 Current streak: {state.get('streak', 0)} days") + print(f"🎯 Level: {state.get('level', 1)}") + print(f"😊 Mood: {state.get('mood', 0)}") + print(f"🎨 Morph: {state.get('morph', 'Banana')}") print(f"📝 Session tasks planned: {state['session_tasks_planned']}") print(f"✅ Session tasks completed: {state['session_tasks_completed']}") From a176a84f0ff8ddc1d8dc6738cebab69f439886bc Mon Sep 17 00:00:00 2001 From: mariajsalgadoq Date: Tue, 4 Nov 2025 14:48:40 -0500 Subject: [PATCH 36/40] Feat: Add get_encouragement function to pet --- Pipfile | 2 +- Pipfile.lock | 124 +++++++++++++++++++++++++++++++-------- study_pet/__main__.py | 9 ++- study_pet/pet/actions.py | 31 ++++++++++ tests/test_actions.py | 38 +++++++++++- 5 files changed, 173 insertions(+), 31 deletions(-) diff --git a/Pipfile b/Pipfile index 538ba9f..c6d4d8a 100644 --- a/Pipfile +++ b/Pipfile @@ -11,4 +11,4 @@ twine = "*" pytest = "*" [requires] -python_version = "3.13" +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 03e8dce..f28f4c8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "e0932273ceead75e720b415b9885af7f9edc69cc362c85ffb8d686cfdb86b623" + "sha256": "cfbfa554d79516631722d4ceee505d08192046094ba323f5696175ebe44b05f2" }, "pipfile-spec": 6, "requires": { - "python_version": "3.13" + "python_version": "3.9" }, "sources": [ { @@ -17,6 +17,14 @@ }, "default": {}, "develop": { + "backports.tarfile": { + "hashes": [ + "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" + ], + "markers": "python_version >= '3.8'", + "version": "==1.2.0" + }, "build": { "hashes": [ "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", @@ -153,14 +161,6 @@ "markers": "python_version >= '3.7'", "version": "==3.4.4" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, "docutils": { "hashes": [ "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", @@ -169,6 +169,14 @@ "markers": "python_version >= '3.9'", "version": "==0.22.2" }, + "exceptiongroup": { + "hashes": [ + "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", + "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, "id": { "hashes": [ "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", @@ -185,13 +193,21 @@ "markers": "python_version >= '3.8'", "version": "==3.11" }, + "importlib-metadata": { + "hashes": [ + "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", + "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" + ], + "markers": "python_version >= '3.9'", + "version": "==8.7.0" + }, "iniconfig": { "hashes": [ - "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", - "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", + "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" ], - "markers": "python_version >= '3.10'", - "version": "==2.3.0" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "jaraco.classes": { "hashes": [ @@ -227,11 +243,11 @@ }, "markdown-it-py": { "hashes": [ - "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", - "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "markers": "python_version >= '3.10'", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "mdurl": { "hashes": [ @@ -322,14 +338,6 @@ "markers": "python_version >= '3.9'", "version": "==8.4.2" }, - "pywin32-ctypes": { - "hashes": [ - "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", - "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" - ], - "markers": "python_version >= '3.6'", - "version": "==0.2.3" - }, "readme-renderer": { "hashes": [ "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", @@ -370,6 +378,54 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, + "tomli": { + "hashes": [ + "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", + "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", + "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", + "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", + "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", + "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", + "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", + "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", + "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", + "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", + "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", + "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", + "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", + "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", + "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", + "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", + "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", + "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", + "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", + "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", + "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", + "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", + "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", + "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", + "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", + "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", + "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", + "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", + "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", + "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", + "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", + "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", + "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", + "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", + "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", + "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", + "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", + "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", + "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", + "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", + "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", + "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", @@ -379,6 +435,14 @@ "markers": "python_version >= '3.9'", "version": "==6.2.0" }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, "urllib3": { "hashes": [ "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", @@ -386,6 +450,14 @@ ], "markers": "python_version >= '3.9'", "version": "==2.5.0" + }, + "zipp": { + "hashes": [ + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + ], + "markers": "python_version >= '3.9'", + "version": "==3.23.0" } } } diff --git a/study_pet/__main__.py b/study_pet/__main__.py index 896df9c..da527bb 100644 --- a/study_pet/__main__.py +++ b/study_pet/__main__.py @@ -1,6 +1,6 @@ from . import start_session, end_session, get_status, reset_pet from .data_manager import load_state, save_state -from .pet import rename_pet, feed_pet, set_morph, check_daily_mood_decay +from .pet import rename_pet, feed_pet, set_morph, check_daily_mood_decay, get_encouragement import argparse import study_pet.tracker as tracker @@ -46,13 +46,16 @@ def actions_menu(): while True: print("\n🐍 Actions Menu:") print("1. Feed your ball python") - print("2. Back") + print("2. Get encouragement") + print("3. Back") - choice = input("\nSelect an option (1–2): ").strip() + choice = input("\nSelect an option (1–3): ").strip() if choice == "1": feed_pet() elif choice == "2": + print(get_encouragement()) + elif choice == "3": break else: print("Invalid option. Try again.") diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index c95116c..813606a 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -260,3 +260,34 @@ def set_morph(morph_name: str = None): print(f"\n✨ Your ball python's morph is now: {selected_morph}!") print(f" {morphs[choice]['desc']}") print("🐍 Your python looks beautiful!\n") + +ENCOURAGEMENT_PHRASES = [ + "You're doing great! Keep up the good work!", + "Every study session makes you smarter!", + "Don't give up! A little more effort!", + "I believe in you! You've got this!", + "Sssslithering sssuccess is just around the corner!", + "Stay focused! You're on a roll!", + "Look at you, being so productive!", + "Your dedication is inspiring!", + "Just think of all the coins you'll earn!", + "Keep going! Your future self will thank you.", +] + +def get_encouragement() -> str: + """ + Returns a random encouraging phrase from the pet. + + The phrase is personalized with the pet's name if it has one. + + Returns: + A string containing a formatted encouragement message. + """ + state = load_state() + # Get the pet's name, or use the default + pet_name = state.get("name", "Guido") + + # Pick a random phrase from the list + phrase = random.choice(ENCOURAGEMENT_PHRASES) + + return f"🐍 {pet_name} says: \"{phrase}\"" diff --git a/tests/test_actions.py b/tests/test_actions.py index b09d450..4f12e85 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,7 +1,10 @@ import sys, os, time, pytest, builtins from datetime import datetime +from unittest.mock import patch sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from study_pet.pet.actions import rename_pet, feed_pet, get_encouragement +from study_pet.data_manager import load_state, save_state, reset_state from study_pet.pet.actions import rename_pet, feed_pet from study_pet.data_manager import load_state, save_state, reset_state @@ -231,4 +234,37 @@ def test_ball_python_theme_in_output(capsys): feed_pet("mouse") captured = capsys.readouterr().out - assert "ball python" in captured.lower() \ No newline at end of file + assert "ball python" in captured.lower() + +def test_get_encouragement_format(monkeypatch): + """Tests the encouragement string is formatted correctly with a pet name.""" + state = {"name": "Monty", "money": 100} + monkeypatch.setattr("study_pet.pet.actions.load_state", lambda: state) + + with patch("random.choice", return_value="You've got this!") as mock_choice: + result = get_encouragement() + mock_choice.assert_called_once() + + assert result == "🐍 Monty says: \"You've got this!\"" + +def test_get_encouragement_default_name(monkeypatch): + """Tests the format using the default name if no name is set.""" + state = {"money": 100} # No 'name' key + monkeypatch.setattr("study_pet.pet.actions.load_state", lambda: state) + + with patch("random.choice", return_value="Keep going!"): + result = get_encouragement() + + assert result == "🐍 Guido says: \"Keep going!\"" + +def test_get_encouragement_returns_string(monkeypatch): + """Tests that the function returns a non-empty string.""" + state = {"name": "Testy"} + monkeypatch.setattr("study_pet.pet.actions.load_state", lambda: state) + + result = get_encouragement() + + assert isinstance(result, str) + assert len(result) > 0 + assert "Testy says:" in result + From 2f14e7cffdda3b9417b857188a49039cddcf0007 Mon Sep 17 00:00:00 2001 From: mariajsalgadoq Date: Tue, 4 Nov 2025 15:55:15 -0500 Subject: [PATCH 37/40] Fix: Export get_encouragement from pet module --- study_pet/pet/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py index c5afe8c..2f47c8d 100644 --- a/study_pet/pet/__init__.py +++ b/study_pet/pet/__init__.py @@ -6,5 +6,6 @@ "rename_pet", "feed_pet", "set_morph", + "get_encouragement", "check_daily_mood_decay", ] From 1b4b558881d5bf58f4def07d062ff99500fd573d Mon Sep 17 00:00:00 2001 From: mariajsalgadoq Date: Tue, 4 Nov 2025 16:05:55 -0500 Subject: [PATCH 38/40] Fix: Correctly export all functions from pet module --- study_pet/pet/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/study_pet/pet/__init__.py b/study_pet/pet/__init__.py index 2f47c8d..abc2a3e 100644 --- a/study_pet/pet/__init__.py +++ b/study_pet/pet/__init__.py @@ -1,5 +1,5 @@ -from .core import get_status, check_daily_mood_decay -from .actions import rename_pet, feed_pet, set_morph +from .core import get_status, check_daily_mood_decay, update_pet +from .actions import rename_pet, feed_pet, set_morph, get_encouragement __all__ = [ "get_status", @@ -8,4 +8,5 @@ "set_morph", "get_encouragement", "check_daily_mood_decay", + "update_pet" ] From d88f42430ed46a0cc3ee65c106d0f9204aeebe81 Mon Sep 17 00:00:00 2001 From: mariajsalgadoq Date: Tue, 4 Nov 2025 16:10:12 -0500 Subject: [PATCH 39/40] Fix: Export functions and add snake phrases --- study_pet/pet/actions.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/study_pet/pet/actions.py b/study_pet/pet/actions.py index 813606a..0456919 100644 --- a/study_pet/pet/actions.py +++ b/study_pet/pet/actions.py @@ -262,16 +262,16 @@ def set_morph(morph_name: str = None): print("🐍 Your python looks beautiful!\n") ENCOURAGEMENT_PHRASES = [ - "You're doing great! Keep up the good work!", - "Every study session makes you smarter!", - "Don't give up! A little more effort!", - "I believe in you! You've got this!", + "You're doing sssso great! Keep up the good work!", + "Every sssstudy ssssession makes you sssmarter!", + "Don't give up! Jussst a little more effort!", + "I believe in you! You've got thisss!", "Sssslithering sssuccess is just around the corner!", - "Stay focused! You're on a roll!", - "Look at you, being so productive!", - "Your dedication is inspiring!", - "Just think of all the coins you'll earn!", - "Keep going! Your future self will thank you.", + "Sssstay focused! You're on a roll!", + "Look at you, being sssso productive!", + "Your dedication is insssspiring!", + "Jussst think of all the coinsss you'll earn!", + "Keep going! Your future ssself will thank you.", ] def get_encouragement() -> str: From 391a217310dc4cbd41f2a2a440ae1d2816d01304 Mon Sep 17 00:00:00 2001 From: mariajsalgadoq Date: Tue, 4 Nov 2025 16:35:30 -0500 Subject: [PATCH 40/40] Fix: Correct imports, add docs, and update snake phrases --- README.md | 8 ++++++++ example.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d159527..b3c12f8 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,14 @@ set_morph("Banana") # Choose from 10 preset morphs set_morph("Custom Morph Name") # Or create your own custom morph # Available presets: Banana, Pastel, Pied, Clown, Mojave, # Cinnamon, Albino, Blue Eyed Leucistic, GHI, Spider + +# Get an encouraging phrase from your pet +encouragement = get_encouragement() +print(encouragement) # Outputs: 🐍 Guido says: "Sssstay focused!" + +# Get your ball python's current status +status = get_status() +print(status) # Returns formatted status string ``` ### Status and Data Functions diff --git a/example.py b/example.py index 373fc96..e3fdf4d 100644 --- a/example.py +++ b/example.py @@ -11,7 +11,7 @@ """ from study_pet.tracker import start_session, end_session -from study_pet.pet import feed_pet, rename_pet, set_morph, get_status +from study_pet.pet import feed_pet, rename_pet, set_morph, get_status, get_encouragement from study_pet.data_manager import load_state @@ -35,6 +35,10 @@ def main(): print("-" * 60) end_session() # Will prompt for completed tasks + print("\n📣 GETTING ENCOURAGEMENT") + print("-" * 60) + print(get_encouragement()) + # Check your ball python's status print("\n📊 CHECKING YOUR BALL PYTHON'S STATUS") print("-" * 60)