From c74a5c3ca73235ff523db1ec700b0f26092e78d4 Mon Sep 17 00:00:00 2001 From: JunHaoChen16 Date: Sun, 2 Nov 2025 11:49:38 -0500 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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 4e94bef98a82d9794db9b0dfe53fb5a35365eaa6 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 3 Nov 2025 23:48:56 +0400 Subject: [PATCH 24/24] 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"])