But I do like Unsloth Studio, quite a lot. It's nicely designed.
Do the runes make it smarter or just run faster (or both)?
https://unsloth.ai/docs/models/glm-5.2#usage-guide
In a prior thread, someone said it would take $500k in hardware:
On top of that, you will still be heavily quantized.
...a bit of an odd question: how well do LLMs losslessly compress, as in for cold storage?
I definitely don't have the hardware to run this model at any kind of reasonable speed (and I don't want to use a super aggressive quantization that would kill performance). Even so, I think it would be cool to retain an offline copy, in case... I don't really know, a solar flare destroys the internet some day, or maybe a zombie apocalypse. It would just be cool to have.
But 1.5 TB is a bit too much! If it could be compressed down into something semi kind of reasonable, that would be fun!
Only a select few have the hardware required to run this to begin with, and even then the forecasted performance makes me wonder if itβs worth it at all.
I am very excited for local LLMs I think we may have GPT 5.5-xhigh level of performance for under 2000 EUR
This should put more pressure on the frontier models to avoid sitting on any fancy stuff and lower token prices as a whole.
Nothing beats a local LLM disconnected from the cloud.
1. Reduce the number of parameters
2. Reduce the resolution of each parameter (quantization)
For 1, changing the architecture is typically only possible by the labs producing the models, which is why each OSS model release tends to feature a small number of carefully chosen model sizes (for example, Gemma4 comes in e2B, e4B, 12B, 26Ba4B, and 31B sizes).
Generally, models with higher parameter counts have more world knowledge. For coding models, this shows up as a stronger command of uncommon libraries/languages. Very small models (<20B) also lack βsmarts.β
Reducing the resolution of each parameter is easier which is why lots of practitioners have their own quantizations, but this makes it harder for a model to βthinkβ fluently. Interacting with heavily quantized models feels like interacting with someone who didnβt get any sleep the night before.
Models that have higher-fidelity quantization take more RAM and have higher βsmarts,β but donβt necessarily have more world knowledge. Models with aggressive quantization tend to be more likely to make rookie mistakes, emit malformed tool calls, get stuck in loops, or even exhibit signs of βneuroticismβ / βdistressβ in their thinking tokens.
Parameter counts = world knowledge, quantization = βsmarts.β
This is a soft rule of thumb, the difference isnβt very strong.
The key to a model this large is (1) Use it to plan, generate lots of plan and farm out to a smaller model. Then for very specific and complicated portions precisely prompt for what you need.
The other way this could go is that Z.ai could decide to release a smaller model targeted towards coding. They've done that before (GLM-4.7-Flash had 30B params). It would be great if they decided to release something in the 80B-100B param range. Something that size would easily run in a current Strix Halo system.
We are maybe 10 years off that.
RAM prices are going to continue to increase for the next 2 years at least.
Even putting that aside it's currently around 40-70,000 EUR to run this with a FP8 quantization (which you need to get close to maximum performance).
To actually get GPT 5.5-xhigh performance in the real world you need more headroom to support things like subagents (which will fill up your KV cache).
I like local models but realism is important. The sweet spot for the next 3 years will continue to be ~35B MoE models. They might match GPT 5.5-xhigh for chat-style problems but not for coding.
Also I m not sure where you are getting the under 2k value. I bought a Framework desktop 128GB last year and my setup was around 2.7k. The same setup now sells for around 4.7k.
NVFP4 at reasonable speeds (~120 tok/s) and concurrency is possible at a $80/90k figure with today's prices, maybe even less. That buys you 6 RTX 6000 PRO Blackwells, a decent CPU and motherboard, power supply. 576gb of VRAM.
You could do it for under $50k if you're OK with 40 tok/s decode, ~1200 tok/s prefill.
I feel like part of the reason for the relative stagnation in hardware over the last twenty years was simply the lack of use cases to justify hardware refreshes by businesses.
Most of the money and energy went to mobile for the last fifteen years.
Affordable local inference might be the gravy train the server, desktop, and laptop manufacturers need to get back in gear.
You can cluster these beasts too. Two and three (with two IP subnets) is fairly obvious. Four or more might need a switch depending on how much network latency affects things.
Apple seem to have forgotten about M series with gobs of RAM. I can't get the Apple shop to show more than 96GB of unified RAM and that costs a kidney.
For companies wanting LLMs in their products in general, I have to think going the local llm route is even more tempting. Somewhat dumb models are more than good enough for a lot of the things people are integrating LLMs into their products.
Official price 85k...
Business hardware got beefier because businesses demanded more data (or more specifically: the industry told businesses they needed more data), with no idea of what to actually do with it once they got it. To get all that data, bandwidth needed to be increased, with more iops to read/write it, more storage to keep it, and more memory and cpu to process it. But 99% of the data is junk. Companies have "data lakes" so big they need to come up with excuses to use the data, or risk somebody pointing out that they're spending a fortune hoarding bits.
Consumer hardware hasn't had a new use case since like 2012. Faster wifi for broadband & local file transfers, and higher-resolution video, are the only reasons one needed newer hardware. We actually got a resolution so high it makes no perceivable difference. And yeah we got faster CPUs and memory, but as soon as we did it got all eaten up by the most inefficient, wasteful software conceivable. Same use cases as 13 years ago, just more expensive, harder to use, and buggier. We should've gotten a new CPU architecture that was faster and more energy efficient. Finally it was delivered, but with a moat around the golden Apple.
Here we are two and a half decades into the Internet era, and my damn bluetooth earbuds and webcam microphone don't work half the time that I open a video conferencing app. Hardware can stay exactly like it is for the next few decades and I'd be happy. I just want software that works, and doesn't get continuously slower, forcing me to buy bigger hardware; or more draconian, locking me out of being able to use it how I want.
Each token has to read all the active weights. I think that's around 40B parameters active. At a 4-bit quant that's 20GB. With 100GB/s (replace with whatever your bandwidth is) and you get 5 tokens per second.
I might build one for fun, but it's not going to change the economics alone. Still exciting it's possible.
(I only ask Opus every 5 to 10 requests, when my local Qwen fails or when I encounter a situation that is too world-knowledge specific to be worth asking, but that way you can live easily with Claude's cheapest plan without ever facing usage limit).
No, we're running into limits of moore's law, and it's showing in prices for new nodes, where they're getting denser but not cheaper.
for $4k, you can get 20 months of claude max 200. i'd take claude over the hardware.
anthropic will have something to worry about when you can run a local model on your macbook that can code. but i think we're quite a ways off from that.
Iβve found that I need to go a couple steps past whatever quantizations are good enough in the KL-divergence testing to get good performance in real tasks with long context. So when Q4 is claimed to be lossless I end up with Q5 or Q6 for actual long-context tasks.
So we hit limits on clock speed in the early 2000s (ex - the 4ghz wall) but it also turned out that mobile as the driver for sales meant no one really cared much about clock speed compared to performance/watt.
Clock speed mattered, but only relative to how many watts it took to get it (and above 4ghz... too many watts).
But we've seen a 15x improvement over the last 20 years. Performance/Watt is WAY up.
My guess is that LLMs are going to drive another "improvement cycle" in areas that we didn't care much about before.
I've built about 10 personal desktop machines (1 every ~4 years) and I can honestly say that I didn't care much about memory bandwidth prior to 2021.
In the same way that I didn't care much about how many watts my pentium 4 was using in 2005.
But now... now I care a lot about memory bandwidth. I care about memory speeds and total system ram in a manner I really, really didn't before.
So I think we're going to see a big shift to machines built on unified ram with a crazy focus on squeezing memory bandwidth and total ram capacity as far as we can.
My bet is that we'll get a similar 10-15x improvement by 2040 in unified system ram designs.
I fully expect to see 2tb unified ram desktops and 200gb unified ram phones be relatively common on a 20 year timeline, assuming we see similar levels of geopolitical stability (ex - world war 3 throws a wrench into things).
(Still potentially very useful! But not magically ultra fast.)
think Apple M6 or M7 with a currently unforeseen denser memory style, 256gb RAM
a couple inference or cache improvements on the algorithmic side, using less ram for context windows and doubling token speed again
denser open source models, packing more experts for smaller active layers
it'll still be expensive but like $8,000 - $13,000 instead of $450,000 worth of B200s
Do not mix the benchmark results of GLM 5.2 FP16/FP8 with FP4 or FP2.
* FP4 will mean a accuracy loss of about 3%. Not noticeable but more chance for mistakes.
* FP2 ... what is what most people are able to run at home, for a "reasonable" price. Your looking at over 17% loss in accuracy.
At that point, your running at less then claude-sonnet-4.6, as the issues compound with accuracy losses. And reasonable priced is still in the ~ $5000 range (192GB + GPU 32GB active/kv cache system).
For that price your using a Codex / Claude Pro subscription for the next 4+ years with better models (by default), let alone with a FP2 GLM 5.2 version. And your looking at < 10 fps. A MacStudio with 512GB will net you 18 a 20fps+ with FP4, but ... i mean, those used to be $10.000.
Unfortunately the local hardware cost is a major issue for running large models like that.
Edit: Its funny whenever the issue of cost and what you need to give up vs the subscription services, there are always people who downvote in bad faith.
In late 2027 or early 2028, Nvidia will release Vera Rubin DGX Spark, likely with double or better the performance of current Blackwell, though unclear if memory capacity will go up much from current 128GB. Two to four of those will run models like this decently.
In 2028 we should expect Vera Rubin RTX discrete lineup, including the replacement to the RTX PRO 6000. Likely memory spec will be minimum 128GB. Good chance of up to 200GB. Two to four of those will run NVFP4 models in this class very well.
LLM companies are valued as if they're going to have some enduring monopoly that they can extract money from... GLM-5.2 and similar models make that valuation very very questionable.
Alibaba released Qwen 3.6 "tiny" models not that long ago, they punch way above their weight(s)
If you are training and doing research it's great, if you want to cluster them it cant be beat, but if you just want local inference on a single box buy a mac or even a strix halo device.
guess we'll be paying $200/month for a while
(it's not our first AI server, we already have experience deploying LLMs for our clients, so the numbers look solid)
i think your answer was perfect not sure why you are being downvoted
No disagreement there, but it could easily last another 3 to 5 years which is a long time in tech terms.
Don't underestimate the markets ability to remain irrational
True, Qwen3.6-27B is amazing for it's size. However, it seems likely that we're not going to see anymore of these smaller models from Alibaba/Qwen since several key players exited that organization a few months back.
I'm a Linux guy, but also don't always have alot of time. The Spark comes out of the box with a nice Linux distro that's pre-configured to be easy to setup and the guides and online resources make getting up and running trivial, for even some complex tasks. You would have to do a LOT of tinkering just to figure out some of the things the nvidia resources walk you through natively. They have guides for a ton of stuff that include the optimal settings so you don't have to figure it all out through trial and error.
Check out these "playbooks" for some examples. [0] There's a lot to be said for not having to piece all that together yourself.
https://build.nvidia.com/spark
I think between unboxing mine setting it up to run headless, and generating tokens was like 20 minutes total for me.
For who pays for it, obviously the employer would.
For "how did I arrive at this number" Ballpark estimate from what I know about part cost. Most of that money will go towards AI cards about $5k for the mb, cpu, power supply, etc. $45k would be for as much ram and as big/expensive nVidia cards as you can get your hands on. The B300 has 288GB of VRAM in it. Probably what you'd be after.
Having said that, I donβt think itβs all that tempting for companies at all, considering this whole market is developing rapidly and itβs nearly impossible to predict where weβll be at in a year or two.
If making RAM and SSDs is now cause for a 10 figure valuation, after enough time somebody will dive in.
a lot of innovation occurring
It's not like you'd lose capabilities, if anything this solution just gets better with time.
The spark can fine tune models in 1/4 the time and excels at other compute tasks in ways that Mac never can. Plus the high bandwidth ConnectX-7 ports would be like $1700 to buy on a card just for the network adapters... But for generating tokens, it just plain loses.
I think youβre better off renting GPU instances and running all the software on those. Itβll be cheaper than Anthropic and OpenRouter but slightly more expensive than electricity and depreciation of hardware.
The number of parameters needed for these open weight models has mostly stabilized so the actual memory requirements aren't likely to change all that much.
βCtrlk
Run the new GLM-5.2 model by Z.ai on local hardware!
GLM-5.2 is Z.aiβs new open model, delivering SOTA performance across long-horizon coding, reasoning, and agentic tasks. With 744B parameters, 40B active parameters, and a 1M context window, it can now be run locally using Unsloth Dynamic GGUFs. GLM-5.2 is the strongest open model to date, performing on par with Claude 4.8 Opus, GPT-5.5, and Gemini 3.1 Pro across Artificial Analysis and many other benchmarks.
Dynamic 1-bit reaches ~76.2% top-1 accuracy while being 86% smaller. Dynamic 2-bit reaches ~82% accuracy while being 84% smaller. This means the model is not 82% worse since it's 84% smaller - it rather is only ~18% less accurate than the full 1.5TB model. Thanks Z.ai for giving Unsloth day-zero access. GLM-5.2-GGUF
Run GLM-5.2 TutorialsQuantization Results
βοΈ Usage Guide
The 2-bit dynamic quant UD-IQ2_M uses 239GB of disk space - this can directly fit on a 256GB unified memory Mac and works well in a 1x24GB GPU and 256GB of RAM with MoE offloading.
The 1-bit quant will fit on a 223GB RAM and 8-bit requires 810GB RAM. See the 1-bit GGUF in action to the right:

Table: Inference hardware requirements (units = total memory: RAM + VRAM, or unified memory)
223 GB
245 GB
290-360 GB
372-475 GB
570 GB
810 GB
For best performance, make sure your total available memory, including VRAM and system RAM, exceeds the quantized model file size by a comfortable margin.
GLM-5.2 has 3 thinking modes. Non-thinking and Thinking in two modes: High + Max. Use Max Thinking for complicated tasks. In Unsloth Studio you can easily toggle High + Max Thinking and non-Thinking with a UI.
Use these settings for most use cases:
temperature = 1.0
temperature = 1.0
top_p = 0.95
top_p = 1.0
1,048,576.GLM 5.2 uses reasoning by default. It also supports reasoning efforts where reasoning_effort can be "high", "max" or disabled.
To disable thinking, use --chat-template-kwargs '{"enable_thinking":false}'. If you're on Windows Powershell, use: --chat-template-kwargs "{\"enable_thinking\":false}"
You can also use --reasoning on or --reasoning off in llama.cpp as well now!
For reasoning effort customization and or to disable reasoning, use the below examples:
We also ran KLD (KL Divergence) benchmarks to gauge the accuracy of our quantizations of GLM-5.2-GGUF. Dynamic 4-bit UD-Q4_K_XL and dynamic 5-bit UD-Q5_K_XL are mostly lossless, and smaller quants also work great by dynamically leaving important layers in higher precision, and unimportant ones to low bits.
On pure top-1% accuracy, dynamic 1-bit gets around 76.2% accuracy yet being 86% smaller! Dynamic 2-bit gets around 82% accuracy whilst being 84% smaller. This shows dynamically quantizing some layers to higher precision does not make the model 86% worse yet being 86% smaller - but only 24% less capable than the full 1.5TB model.
But what does "76% accuracy" actually describe?
76% top-1% does NOT mean "The capital of France is" becomes choosing 76% Paris and 24% of Sydney. The model is NOT "dumber" by 24%. For this, Paris is always 100%, and Sydney is 0%. The 76% number includes filler words and stop words across the entire corpus for example asking:
"Create a novel" will get due to LLM sampling:
I will now create a novel...
The novel is below:
What genre would you like it to be?
Each example is correct, but the [I, The, What] distribution is what changes - the baseline might use [I] 100% of the time, but now [I] is 76% and [The] is 24%.
It does NOT mean that you get incorrect outputs like gibberish or incorrect outputs 24% of the time.

99.9% KLD is also generally good - there is a larger uplift from 4bit onwards though, so for massive out of distribution tasks, dynamic 4-bit is probably best.
Top-1% is a "forced" binomial distrution of the KLD itself. KLD is the "distance" between the probabilities of the baseline (BF16 or Q8_0) vs the quantized version. The goal of quantization is to minimize the below objective:
minimize 1nβDKL[ f(q(W)) β£β£ f(W)) ]\text{minimize } \frac{1}{n} \sum{\text{D}_{\text{KL}}[\text{ }f(q(W))\text{ }||\text{ } f(W))\text{ }]}minimize n1ββDKLβ[ f(q(W)) β£β£ f(W)) ]
Where f is the language model's forward and q is the quantization operation and W is the parameters or weights of the model. The goal is to make the "distance" between the logits output of the baseline f(W) and the quantized model's output as small as possible. If you can make 0 KLD, then you have perfectly reconstructed the model!

We use mean KLD like below since it's expensive to run KLD across the full training corpus (15T tokens for example) - instead we do sampling, and get a small representative subset of the training corpus / downstream task, and optimize that. Mean KLD generally follows a monotonic trend vs disk space, and shows even at 1-bit GLM 5.2 works well!
Top-1% accuracy is simply a greedy decoding operator where we assume the argmax item will be picked, and for 1bit, 76% it picks the same as the argmax from the baseline.

You can now run GLM-5.2 in llama.cpp and Unsloth Studio. We will be utilizing the 239GB UD-IQ2_M quant for best results in terms of accessbility and accuracy.
GLM-5.2 can run in Unsloth Studio, an open-source web UI for local AI. Unsloth Studio automatically offloads to RAM and detects multiGPU setups. With Unsloth Studio, you can run models locally on MacOS, Windows, Linux and:
Search, download, run GGUFs and safetensor models
Fast CPU + GPU inference via llama.cpp

2-bit GLM-5.2 GGUF output
Install and Launch Unsloth
To install, run in your terminal:
MacOS, Linux, WSL:
Windows PowerShell:
Launch Unsloth
MacOS, Linux, WSL and Windows:
Then open http://127.0.0.1:8888 (or your specific URL) in your browser.
Launch Unsloth securely with HTTPS and Cloudflare
NEW! Unsloth now provides a secure way to launch Studio over HTTPS through a free Cloudflare tunnel. Use the below (works in Windows, Mac & Linux):
Search and download GLM-5.2
Unsloth Studio automatically offloads to RAM and detects multiGPU setups. On first launch you will need to create a password to secure your account and sign in again later.
Then go to the Studio Chat tab and search for GLM-5.2 in the search bar and download your desired model and quant. Ensure you have enough compute the run the model.

Run GLM-5.2
Inference parameters should be auto-set when using Unsloth Studio, however you can still change it manually. You can also edit the context length, chat template and other settings.
For more information, you can view our Unsloth Studio inference guide.

Example of 1-bit GLM-5.2 running with Canvas
For this guide we'll be running the UD-IQ2_M quant which will require at least 245GB RAM. Feel free to change quantization type. For these tutorials, we will using llama.cpp for fast local inference. GGUF: GLM-5.2-GGUF
Obtain the latest llama.cpp on GitHub here. You can follow the build instructions below as well. Change -DGGML_CUDA=ON to -DGGML_CUDA=OFF if you don't have a GPU or just want CPU inference. For Apple Mac / Metal devices, set -DGGML_CUDA=OFF then continue as usual - Metal support is on by default.
You can now use llama.cpp directly to load and download models, just like ollama run. First, select the quantization type you want like UD-IQ2_M. Also use export LLAMA_CACHE="unsloth/GLM-5.2-GGUF" to force llama.cpp to save to a specific location. Note this download process might be very slow, so it's probably best to use the manual download process in the next section.
If you want to download the model manually (much faster!), we can download the model via the code below (after installing pip install huggingface_hub). If downloads get stuck, see: Hugging Face Hub, XET debugging
If you want to use the dynamic 1bit, then do:
Then run the model in conversation mode. Use unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf for 2bit or unsloth/GLM-5.2-GGUF/UD-IQ1_S/GLM-5.2-UD-IQ1_S-00001-of-00006.gguf for 1bit.
When you launch llama-cli, you will see:

Then after prompting it to make a short Flappy Bird game, we get:

With the full conversation and game below:
Full game in HTMLFull conversation
And the game has sound and works wonderfully! Reminder this was a 1-bit quantization and it worked well!

πLong context via KV Cache quantization
To utilize long context in llama.cpp, we need to employ KV cache quantization to reduce memory usage. Recently llama.cpp added higher accuracy tricks to KV cache quantization - see and other PRs!
Currently, these KV cache dtypes are supported:
By default f16 is used. If you use q4_0 which is around 4.5 bits per weight, you can extend around 16 / 4.5 = 3.5x longer context lengths! So if you model used to support 10K, 35K can be in reach! q4_1 is probably better since you also get a shifting parameter, and is 5 bits per weight - so 3.2x longer contexts.
Use it like below:
You can view further below for GLM-5.2 benchmarks in table format:
Reasoning
HLE
40.5
49.8*
41.4*
45
31
41.4
37
37.7
HLE (w/ Tools)
54.7
57.9*
52.2*
51.4*
52.3
53.5
-
48.2
CritPt
20.9
20.9
27.1
17.7
4.6
13.4
3.7
12.9
AIME 2026
99.2
95.7
98.3
98.2
95.3
97
-
94.6
HMMT Nov. 2025
94.4
96.5
96.5
94.8
94
95
84.4
94.4
HMMT Feb. 2026
92.5
96.7
96.7
87.3
82.6
97.1
84.4
95.2
IMOAnswerBench
91.0
83.5
-
81
83.8
90
-
89.8
GPQA-Diamond
91.2
93.6
93.6
94.3
86.2
90
93
90.1
Coding
SWE-bench Pro
62.1
69.2
58.6
54.2
58.4
60.6
59
55.4
NL2Repo
48.9
69.7
50.7
33.4
42.7
47.2
42.1
35.5
DeepSWE
46.2
58
70
10
18
18
20
8
ProgramBench
63.7
71.9
70.8
39.5
50.9
-
-
47.8
Terminal Bench 2.1 (Terminus-2)
81.0
85
84
74
63.5
75
65
64
Terminal Bench 2.1 (Best Reported Harness)
82.7
78.9
83.4
70.7
69
-
-
-
FrontierSWE (Dominance)
74.4
75.1
72.6
39.6
30.5
-
-
29.0
PostTrainBench
34.3
37.2
28.4
21.6
20.1
-
-
-
SWE-Marathon
13.0
26.0
12.0
4.0
1.0
-
-
-
Agentic
MCP-Atlas (Public Set)
76.8
77.8
75.3
69.2
71.8
76.4
74.2
73.6
Tool-Decathlon
48.2
59.9
55.6
48.8
40.7
-
-
52.8
Last updated 2 minutes ago
Was this helpful?
Was this helpful?
--chat-template-kwargs '{"reasoning_effort":"max"}'
--chat-template-kwargs '{"reasoning_effort":"high"}'
--chat-template-kwargs '{"enable_thinking":false}'
curl -fsSL https://unsloth.ai/install.sh | sh
irm https://unsloth.ai/install.ps1 | iex
unsloth studio -H 0.0.0.0 -p 8888
unsloth studio --secure
apt-get update
apt-get install pciutils build-essential cmake curl libcurl4-openssl-dev -y
git clone https://github.com/ggml-org/llama.cpp
cmake llama.cpp -B llama.cpp/build \
-DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON
cmake --build llama.cpp/build --config Release -j --clean-first --target llama-cli llama-mtmd-cli llama-server llama-gguf-split
cp llama.cpp/build/bin/llama-* llama.cpp
export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"
./llama.cpp/llama-cli \
-hf unsloth/GLM-5.2-GGUF:UD-IQ2_M \
--temp 1.0 \
--top-p 0.95 \
--min-p 0.01
hf download unsloth/GLM-5.2-GGUF \
--local-dir unsloth/GLM-5.2-GGUF \
--include "*UD-IQ2_M*" # Use "*UD-Q8_K_XL*" for near full precision
hf download unsloth/GLM-5.2-GGUF \
--local-dir unsloth/GLM-5.2-GGUF \
--include "*UD-IQ1_S*"
./llama.cpp/llama-cli \
--model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \
--temp 1.0 \
--top-p 0.95 \
--min-p 0.01
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Sunset Flier</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--sunset-1: #ff6b6b;
--sunset-2: #feca50;
--sunset-3: #ff9ff3;
--dusk: #36306b;
--night: #1a1746;
--accent: #ffd93b;
--coral: #ff6b6b;
--pipe: #4a902b;
--pipe-dark: #2d5a1a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
width: 100%;
overflow: hidden;
background: var(--night);
font-family: 'Fred', sans-serif;
-webkit-user-select: none;
user-select: none;
touch-action: manipulation;
}
#game-wrap {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
}
#game-frame {
position: relative;
width: min(100vw, 480px);
height: min(100vh, 720px);
max-height: 100vh;
box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden;
background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
transition: opacity 0.3s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay.visible {
opacity: 1;
pointer-events: auto;
}
.panel {
background: rgba(26, 23, 70, 0.85);
border: 3px solid var(--accent);
border-radius: 16px;
padding: 28px 36px;
text-align: center;
color: #fff;
box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
backdrop-filter: blur(4px);
transform: translateY(0);
animation: bob 3s ease-in-out infinite;
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.title {
font-family: 'Press Start 2P', monospace;
font-size: 26px;
color: var(--accent);
text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
letter-spacing: 1px;
margin-bottom: 6px;
line-height: 1.3;
}
.subtitle {
font-size: 14px;
color: #ffe8a8;
margin-bottom: 20px;
font-weight: 700;
}
.tap-icon {
font-size: 42px;
margin: 8px 0;
animation: tap 1.2s ease-in-out infinite;
}
@keyframes tap {
0%, 50% { transform: translateY(0) scale(1); }
20% { transform: translateY(-8px) scale(1.1); }
}
.instructions {
font-size: 13px;
color: #fff;
opacity: 0.85;
margin-top: 10px;
font-weight: 400;
}
.score-row {
display: flex;
gap: 24px;
justify-content: center;
margin: 12px 0 20px;
}
.score-box {
background: rgba(0,0,0,0.4);
border: 2px solid var(--accent);
border-radius: 10px;
padding: 10px 18px;
min-width: 80px;
}
.score-box .label {
font-family: 'Press Start 2p', monospace;
font-size: 9px;
color: var(--accent);
margin-bottom: 4px;
letter-spacing: 1px;
}
.score-box .value {
font-family: 'Press Start 2p', monospace;
font-size: 18px;
color: #fff;
}
.score-box.best .value { color: var(--coral); }
.btn {
font-family: 'Press Start 2p', monospace;
font-size: 12px;
color: var(--night);
background: var(--accent);
border: none;
padding: 12px 22px;
border-radius: 8px;
cursor: pointer;
letter-spacing: 1px;
box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
transition: transform 0.1s, box-shadow 0.1s;
pointer-events: auto;
}
.btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
.btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }
#hud {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 5;
font-family: 'Press Start 2P', monospace;
font-size: 36px;
color: #fff;
text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
pointer-events: none;
transition: opacity 0.3s;
opacity: 0;
}
#hud.visible { opacity: 1; }
#hud .new-best {
font-size: 11px;
color: var(--coral);
text-shadow: 2px 2px 0 #000;
margin-top: 8px;
opacity: 0;
transition: opacity 0.3s;
}
#hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }
@keyframes pulse {
from { transform: translateX(-50%) scale(1); }
to { transform: translateX(-50%) scale(1.15); }
}
.medal {
font-family: 'Press Start 2P', monospace;
font-size: 48px;
margin: 10px 0;
text-shadow: 3px 3px 0 #000;
}
.flash {
position: absolute;
inset: 0;
background: #fff;
opacity: 0;
pointer-events: none;
z-index: 8;
}
.footer {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(255,255,255,0.7);
font-weight: 400;
text-align: center;
pointer-events: none;
z-index: 9;
}
</style>
</head>
<body>
<div id="game-wrap">
<div id="game-frame">
<canvas id="canvas"></canvas>
<div id="hud" class="hud">
<div id="hud-score">0</div>
<div class="new-best">NEW BEST!</div>
</div>
<div id="flash" class="flash"></div>
<!-- Start screen -->
<div id="start-screen" class="overlay visible">
<div class="panel">
<div class="title">SUNSET<br>FLIER</div>
<div class="subtitle">β dusk flight β</div>
<div class="tap-icon">β</div>
<div class="instructions">TAP / SPACE / CLICK to flap</div>
</div>
</div>
<!-- Game over screen -->
<div id="end-screen" class="overlay hidden">
<div class="panel">
<div class="title" style="font-size:22px;color:var(--coral);">GAME OVER</div>
<div id="medal" class="medal" style="display:none;">β
</div>
<div class="score-row">
<div class="score-box">
<div class="label">SCORE</div>
<div class="value" id="end-score">0</div>
</div>
<div class="score-box best">
<div class="label">BEST</div>
<div class="value" id="end-best">0</div>
</div>
</div>
<button class="btn" id="restart-btn">RETRY</button>
</div>
</div>
<div class="footer">Sound on β’ Tap to fly</div>
</div>
</div>
<script>
(() => {
// ============ Setup ============
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const frame = document.getElementById('game-frame');
const hud = document.getElementById('hud');
const hudScore = document.getElementById('hud-score');
const startScreen = document.getElementById('start-screen');
const endScreen = document.getElementById('end-screen');
const endScore = document.getElementById('end-score');
const endBest = document.getElementById('end-best');
const medalEl = document.getElementById('medal');
const flashEl = document.getElementById('flash');
const restartBtn = document.getElementById('restart-btn');
let W = 480, H = 720;
const dpr = window.devicePixelRatio || 1;
function resize() {
const rect = frame.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ============ Audio (Web Audio API) ============
let audioCtx = null;
function getAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch(e) { audioCtx = null; }
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
const ac = getAudio();
if (!ac) return;
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.frequency.value = freq;
if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
gain.gain.value = vol;
gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
osc.connect(gain);
gain.connect(ac.destination);
osc.start();
osc.stop(ac.currentTime + dur);
}
const sfx = {
flap: () => beep(420, 0.12, 'square', 0.08, 720),
score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); },
hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); },
die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
};
// ============ Game State ============
const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
let state = STATE.READY;
let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;
best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
endBest.textContent = best;
// ============ Bird ============
function makeBird() {
return {
x: W * 0.32,
y: H * 0.5,
vy: 0,
radius: 14,
wingPhase: 0,
rotation: 0,
alive: true
};
}
// ============ Pipes ============
const PIPE_W = 68;
const PIPE_GAP = 180;
const PIPE_SPEED = 2.6;
const PIPE_SPACING = 220;
function makePipe(x) {
const minY = 90;
const maxY = H - 90 - PIPE_GAP;
const gapY = minY + Math.random() * (maxY - minY);
return { x, gapY, scored: false, passed: false };
}
function initPipes() {
pipes = [];
let x = W + 80;
for (let i = 0; i < 4; i++) {
pipes.push(makePipe(x));
x += PIPE_SPACING;
}
}
// ============ Particles ============
function spawnParticles(x, y, color, count=10, vel=3) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.6 + 0.4) * vel;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
life: 1,
size: Math.random() * 4 + 2,
color
});
}
}
// ============ Background ============
function initBg() {
clouds = [];
for (let i = 0; i < 4; i++) {
clouds.push({
x: Math.random() * W,
y: 80 + Math.random() * 180,
scale: 0.6 + Math.random() * 0.8,
speed: 0.2 + Math.random() * 0.3
});
}
stars = [];
for (let i = 0; i < 30; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.4,
r: Math.random() * 1.5 + 0.3,
tw: Math.random() * Math.PI * 2
});
}
}
// ============ Reset / Start ============
function reset() {
bird = makeBird();
pipes = [];
particles = [];
score = 0;
frameCount = 0;
shake = 0;
groundX = 0;
initPipes();
initBg();
hudScore.textContent = '0';
hud.classList.remove('has-new-best');
}
function startGame() {
state = STATE.PLAYING;
startScreen.classList.add('hidden');
startScreen.classList.remove('visible');
hud.classList.add('visible');
bird.vy = -7.2;
sfx.flap();
}
function gameOver() {
state = STATE.DYING;
sfx.hit();
setTimeout(sfx.die, 120);
shake = 18;
spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
flashScreen();
bird.alive = false;
const isNewBest = score > best;
if (isNewBest) {
best = score;
localStorage.setItem('sunsetFlierBest', String(best));
}
setTimeout(() => {
state = STATE.OVER;
endScore.textContent = score;
endBest.textContent = best;
const medal = getMedal(score);
if (medal) {
medalEl.style.display = 'block';
medalEl.textContent = medal.symbol;
medalEl.style.color = medal.color;
} else {
medalEl.style.display = 'none';
}
endScreen.classList.remove('hidden');
endScreen.classList.add('visible');
if (isNewBest) {
setTimeout(()=>hud.classList.add('has-new-best'), 0);
}
}, 700);
}
function flashScreen() {
flashEl.style.transition = 'none';
flashEl.style.opacity = '0.9';
requestAnimationFrame(() => {
flashEl.style.transition = 'opacity 0.4s ease-out';
flashEl.style.opacity = '0';
});
}
function getMedal(s) {
if (s >= 30) return { symbol: 'β
', color: '#ffd93b' };
if (s >= 20) return { symbol: 'β¦', color: '#c0c0c0' };
if (s >= 10) return { symbol: 'β§', color: '#cd7f32' };
if (s >= 5) return { symbol: 'β’', color: '#ff9ff3' };
return null;
}
function restart() {
reset();
state = STATE.READY;
endScreen.classList.add('hidden');
endScreen.classList.remove('visible');
startScreen.classList.remove('hidden');
startScreen.classList.add('visible');
hud.classList.remove('visible');
hud.classList.remove('has-new-best');
}
// ============ Input ============
function flap() {
if (state === STATE.READY) { startGame(); return; }
if (state === STATE.PLAYING) {
bird.vy = -7.2;
bird.wingPhase = 0;
sfx.flap();
spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
}
if (state === STATE.OVER) {
// tap anywhere on end screen also restarts
// but we let button handle it primarily
}
}
function handleInput(e) {
if (e.target.closest('button')) return;
e.preventDefault();
flap();
}
frame.addEventListener('mousedown', handleInput);
frame.addEventListener('touchstart', handleInput, { passive: false });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
e.preventDefault();
if (state === STATE.OVER) restart();
else flap();
}
});
restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });
// ============ Update ============
const GRAVITY = 0.42;
const MAX_FALL = 9;
function update() {
frameCount++;
if (state === STATE.READY) {
// idle bob
bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
bird.wingPhase += 0.2;
bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
}
if (state === STATE.PLAYING) {
bird.vy += GRAVITY;
if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
bird.y += bird.vy;
bird.wingPhase += 0.4;
bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));
// Pipes
for (const p of pipes) {
p.x -= PIPE_SPEED;
if (!p.scored && p.x + PIPE_W < bird.x) {
p.scored = true;
score++;
hudScore.textContent = score;
sfx.score();
spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
}
}
// Remove off-screen pipes, add new
while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
const last = pipes[pipes.length - 1];
if (last && last.x < W - PIPE_SPACING) {
pipes.push(makePipe(last.x + PIPE_SPACING));
}
// Collision
// Ground & ceiling
const groundY = H - 80;
if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
gameOver();
return;
}
// Pipes
for (const p of pipes) {
if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
const top = p.gapY;
const bottom = p.gapY + PIPE_GAP;
if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
// collision
gameOver();
return;
}
}
}
if (state === STATE.DYING) {
bird.vy += GRAVITY * 1.2;
bird.y += bird.vy;
bird.rotation += 0.06;
const groundY = H - 80;
if (bird.y + bird.radius > groundY) {
bird.y = groundY - bird.radius;
bird.vy = 0;
}
}
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life -= 0.02;
if (p.life <= 0) particles.splice(i, 1);
}
// Shake decay
if (shake > 0) shake = Math.max(0, shake - 1.2);
// Background animations
for (const c of clouds) {
c.x -= c.speed;
if (c.x < -100) c.x = W + 100;
}
for (const s of stars) {
s.tw += 0.05;
}
groundX = (groundX - PIPE_SPEED) % 120;
}
// ============ Render ============
function render() {
ctx.save();
// Shake
if (shake > 0) {
const sx = (Math.random() - 0.5) * shake;
const sy = (Math.random() - 0.5) * shake;
ctx.translate(sx, sy);
}
// Sky gradient
const sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, '#1a1746');
sky.addColorStop(0.3, '#4a3a8e');
sky.addColorStop(0.55, '#ff6b6b');
sky.addColorStop(0.8, '#feca50');
sky.addColorStop(1, '#ffd93b');
ctx.fillStyle = sky;
ctx.fillRect(-20, -20, W + 40, H + 40);
// Stars
for (const s of stars) {
const a = 0.4 + Math.sin(s.tw) * 0.4;
ctx.fillStyle = `rgba(255,255,255,${a})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunX = W * 0.75;
const sunY = H * 0.65;
const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(sunX - 200, sunY - 200, 400, 400);
ctx.fillStyle = '#fff6d0';
ctx.beginPath();
ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
ctx.fill();
// Clouds
for (const c of clouds) {
drawCloud(c.x, c.y, c.scale);
}
// Mountains silhouette
ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
ctx.beginPath();
ctx.moveTo(0, H * 0.72);
const peaks = [
[W * 0.15, H * 0.58],
[W * 0.25, H * 0.68],
[W * 0.4, H * 0.52],
[W * 0.55, H * 0.66],
[W * 0.7, H * 0.6],
[W * 0.85, H * 0.68],
[W, H * 0.62]
];
for (const [px, py] of peaks) ctx.lineTo(px, py);
ctx.lineTo(W, H);
ctx.lineTo(0, H);
ctx.closePath();
ctx.fill();
// Pipes
for (const p of pipes) {
drawPipe(p);
}
// Ground
drawGround();
// Bird
drawBird();
// Particles
for (const part of particles) {
ctx.fillStyle = part.color;
ctx.globalAlpha = part.life;
ctx.beginPath();
ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawCloud(x, y, scale) {
ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
const s = scale;
ctx.beginPath();
ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
ctx.fill();
}
function drawPipe(p) {
const x = p.x;
const topY = p.gapY;
const bottomY = p.gapY + PIPE_GAP;
const groundY = H - 80;
// gradient
const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
grad.addColorStop(0, '#2d5a1a');
grad.addColorStop(0.15, '#4a902b');
grad.addColorStop(0.5, '#7bc94a');
grad.addColorStop(0.85, '#4a902b');
grad.addColorStop(1, '#2d5a1a');
// Top pipe
ctx.fillStyle = grad;
ctx.fillRect(x, 0, PIPE_W, topY - 18);
// Top cap
ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
// Top cap highlight
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, topY - 18, 14, 18);
// Bottom pipe
ctx.fillStyle = grad;
ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
// Bottom cap
ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, bottomY, 14, 18);
// Outline
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 2;
ctx.strokeRect(x, 0, PIPE_W, topY - 18);
ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
}
function drawGround() {
const groundY = H - 80;
// Sand/dusk ground
const g = ctx.createLinearGradient(0, groundY, 0, H);
g.addColorStop(0, '#8b5a2b');
g.addColorStop(0.4, '#5e3a1c');
g.addColorStop(1, '#2d1a0c');
ctx.fillStyle = g;
ctx.fillRect(0, groundY, W, 80);
// surface line
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(W, groundY);
ctx.stroke();
// Pattern stripes (moving)
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let i = -2; i < W / 40 + 2; i++) {
const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
ctx.beginPath();
ctx.moveTo(stripeX, groundY);
ctx.lineTo(stripeX + 30, groundY + 80);
ctx.lineTo(stripeX + 50, groundY + 80);
ctx.lineTo(stripeX + 20, groundY);
ctx.closePath();
ctx.fill();
}
// Pebbles on top
ctx.fillStyle = '#3d2412';
for (let i = 0; i < 12; i++) {
const px = (i * 60 + groundX + 60) % (W + 40);
ctx.beginPath();
ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawBird() {
const b = bird;
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.rotation);
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
ctx.fill();
// Body
const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
bodyGrad.addColorStop(0, '#ff9ff3');
bodyGrad.addColorStop(0.5, '#ff6b6b');
bodyGrad.addColorStop(1, '#c0334a');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
ctx.fill();
// Wing (flapping)
const wingY = Math.sin(b.wingPhase) * 8;
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1.5;
ctx.stroke();
// Eye
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(8, -4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1a1746';
ctx.beginPath();
ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(10, -5, 1, 0, Math.PI * 2);
ctx.fill();
// Beak
ctx.fillStyle = '#ffaa33';
ctx.beginPath();
ctx.moveTo(13, -1);
ctx.lineTo(20, 1);
ctx.lineTo(13, 3);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1;
ctx.stroke();
// Tail
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.moveTo(-12, -2);
ctx.lineTo(-18, -5);
ctx.lineTo(-16, 0);
ctx.lineTo(-18, 5);
ctx.lineTo(-12, 3);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
// ============ Loop ============
function loop() {
update();
render();
requestAnimationFrame(loop);
}
reset();
loop();
})();
</script>
</body>
</html>
[Start thinking]
The user wants a short Flappy Bird game. I'll create a complete, self-contained HTML file with:
1. A classic Flappy Bird-style game
2. Nice graphics and animations
3. Score tracking
4. Game over screen
5. Restart functionality
6. Good physics (gravity, jump mechanics)
7. Pipes that move and are randomly placed
8. Sound effects (using Web Audio API for tones)
9. A nice visual style - I'll avoid the classic yellow bird and use a more interesting color scheme
Let me design this with:
- A nice gradient sky background
- Animated bird with flapping wings
- Pipes with a nice green/metallic look
- Score display
- Start screen, playing state, game over state
- High score tracking with localStorage
- Particle effects when scoring
- Smooth animations
I'll use a sunset/dusk color palette to make it distinctive - warm oranges and purples, with the bird being a warm coral/red color.
Let me code this up carefully:
Key features:
- Canvas-based rendering
- requestAnimationFrame game loop
- Bird physics: gravity, jump impulse
- Pipe generation: random gaps, moving left
- Collision detection
- Score increment when passing pipes
- Particle effects on score
- Death animation
- localStorage for high score
- Sound effects via Web Audio API
- Mobile/touch support
- Clean, polished UI overlay
Let me write the code.
[End thinking]
I'll create a polished Flappy Bird game with a distinctive dusk/sunset aesthetic, smooth physics, particle effects, and sound.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Sunset Flier</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--sunset-1: #ff6b6b;
--sunset-2: #feca50;
--sunset-3: #ff9ff3;
--dusk: #36306b;
--night: #1a1746;
--accent: #ffd93b;
--coral: #ff6b6b;
--pipe: #4a902b;
--pipe-dark: #2d5a1a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
width: 100%;
overflow: hidden;
background: var(--night);
font-family: 'Fred', sans-serif;
-webkit-user-select: none;
user-select: none;
touch-action: manipulation;
}
#game-wrap {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
}
#game-frame {
position: relative;
width: min(100vw, 480px);
height: min(100vh, 720px);
max-height: 100vh;
box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden;
background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
transition: opacity 0.3s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay.visible {
opacity: 1;
pointer-events: auto;
}
.panel {
background: rgba(26, 23, 70, 0.85);
border: 3px solid var(--accent);
border-radius: 16px;
padding: 28px 36px;
text-align: center;
color: #fff;
box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
backdrop-filter: blur(4px);
transform: translateY(0);
animation: bob 3s ease-in-out infinite;
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.title {
font-family: 'Press Start 2P', monospace;
font-size: 26px;
color: var(--accent);
text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
letter-spacing: 1px;
margin-bottom: 6px;
line-height: 1.3;
}
.subtitle {
font-size: 14px;
color: #ffe8a8;
margin-bottom: 20px;
font-weight: 700;
}
.tap-icon {
font-size: 42px;
margin: 8px 0;
animation: tap 1.2s ease-in-out infinite;
}
@keyframes tap {
0%, 50% { transform: translateY(0) scale(1); }
20% { transform: translateY(-8px) scale(1.1); }
}
.instructions {
font-size: 13px;
color: #fff;
opacity: 0.85;
margin-top: 10px;
font-weight: 400;
}
.score-row {
display: flex;
gap: 24px;
justify-content: center;
margin: 12px 0 20px;
}
.score-box {
background: rgba(0,0,0,0.4);
border: 2px solid var(--accent);
border-radius: 10px;
padding: 10px 18px;
min-width: 80px;
}
.score-box .label {
font-family: 'Press Start 2p', monospace;
font-size: 9px;
color: var(--accent);
margin-bottom: 4px;
letter-spacing: 1px;
}
.score-box .value {
font-family: 'Press Start 2p', monospace;
font-size: 18px;
color: #fff;
}
.score-box.best .value { color: var(--coral); }
.btn {
font-family: 'Press Start 2p', monospace;
font-size: 12px;
color: var(--night);
background: var(--accent);
border: none;
padding: 12px 22px;
border-radius: 8px;
cursor: pointer;
letter-spacing: 1px;
box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
transition: transform 0.1s, box-shadow 0.1s;
pointer-events: auto;
}
.btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
.btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }
#hud {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 5;
font-family: 'Press Start 2P', monospace;
font-size: 36px;
color: #fff;
text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
pointer-events: none;
transition: opacity 0.3s;
opacity: 0;
}
#hud.visible { opacity: 1; }
#hud .new-best {
font-size: 11px;
color: var(--coral);
text-shadow: 2px 2px 0 #000;
margin-top: 8px;
opacity: 0;
transition: opacity 0.3s;
}
#hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }
@keyframes pulse {
from { transform: translateX(-50%) scale(1); }
to { transform: translateX(-50%) scale(1.15); }
}
.medal {
font-family: 'Press Start 2P', monospace;
font-size: 48px;
margin: 10px 0;
text-shadow: 3px 3px 0 #000;
}
.flash {
position: absolute;
inset: 0;
background: #fff;
opacity: 0;
pointer-events: none;
z-index: 8;
}
.footer {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(255,255,255,0.7);
font-weight: 400;
text-align: center;
pointer-events: none;
z-index: 9;
}
</style>
</head>
<body>
<div id="game-wrap">
<div id="game-frame">
<canvas id="canvas"></canvas>
<div id="hud" class="hud">
<div id="hud-score">0</div>
<div class="new-best">NEW BEST!</div>
</div>
<div id="flash" class="flash"></div>
<!-- Start screen -->
<div id="start-screen" class="overlay visible">
<div class="panel">
<div class="title">SUNSET<br>FLIER</div>
<div class="subtitle">β dusk flight β</div>
<div class="tap-icon">β</div>
<div class="instructions">TAP / SPACE / CLICK to flap</div>
</div>
</div>
<!-- Game over screen -->
<div id="end-screen" class="overlay hidden">
<div class="panel">
<div class="title" style="font-size:22px;color:var(--coral);">GAME OVER</div>
<div id="medal" class="medal" style="display:none;">β
</div>
<div class="score-row">
<div class="score-box">
<div class="label">SCORE</div>
<div class="value" id="end-score">0</div>
</div>
<div class="score-box best">
<div class="label">BEST</div>
<div class="value" id="end-best">0</div>
</div>
</div>
<button class="btn" id="restart-btn">RETRY</button>
</div>
</div>
<div class="footer">Sound on β’ Tap to fly</div>
</div>
</div>
<script>
(() => {
// ============ Setup ============
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const frame = document.getElementById('game-frame');
const hud = document.getElementById('hud');
const hudScore = document.getElementById('hud-score');
const startScreen = document.getElementById('start-screen');
const endScreen = document.getElementById('end-screen');
const endScore = document.getElementById('end-score');
const endBest = document.getElementById('end-best');
const medalEl = document.getElementById('medal');
const flashEl = document.getElementById('flash');
const restartBtn = document.getElementById('restart-btn');
let W = 480, H = 720;
const dpr = window.devicePixelRatio || 1;
function resize() {
const rect = frame.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ============ Audio (Web Audio API) ============
let audioCtx = null;
function getAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch(e) { audioCtx = null; }
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
const ac = getAudio();
if (!ac) return;
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.frequency.value = freq;
if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
gain.gain.value = vol;
gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
osc.connect(gain);
gain.connect(ac.destination);
osc.start();
osc.stop(ac.currentTime + dur);
}
const sfx = {
flap: () => beep(420, 0.12, 'square', 0.08, 720),
score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); },
hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); },
die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
};
// ============ Game State ============
const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
let state = STATE.READY;
let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;
best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
endBest.textContent = best;
// ============ Bird ============
function makeBird() {
return {
x: W * 0.32,
y: H * 0.5,
vy: 0,
radius: 14,
wingPhase: 0,
rotation: 0,
alive: true
};
}
// ============ Pipes ============
const PIPE_W = 68;
const PIPE_GAP = 180;
const PIPE_SPEED = 2.6;
const PIPE_SPACING = 220;
function makePipe(x) {
const minY = 90;
const maxY = H - 90 - PIPE_GAP;
const gapY = minY + Math.random() * (maxY - minY);
return { x, gapY, scored: false, passed: false };
}
function initPipes() {
pipes = [];
let x = W + 80;
for (let i = 0; i < 4; i++) {
pipes.push(makePipe(x));
x += PIPE_SPACING;
}
}
// ============ Particles ============
function spawnParticles(x, y, color, count=10, vel=3) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.6 + 0.4) * vel;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
life: 1,
size: Math.random() * 4 + 2,
color
});
}
}
// ============ Background ============
function initBg() {
clouds = [];
for (let i = 0; i < 4; i++) {
clouds.push({
x: Math.random() * W,
y: 80 + Math.random() * 180,
scale: 0.6 + Math.random() * 0.8,
speed: 0.2 + Math.random() * 0.3
});
}
stars = [];
for (let i = 0; i < 30; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.4,
r: Math.random() * 1.5 + 0.3,
tw: Math.random() * Math.PI * 2
});
}
}
// ============ Reset / Start ============
function reset() {
bird = makeBird();
pipes = [];
particles = [];
score = 0;
frameCount = 0;
shake = 0;
groundX = 0;
initPipes();
initBg();
hudScore.textContent = '0';
hud.classList.remove('has-new-best');
}
function startGame() {
state = STATE.PLAYING;
startScreen.classList.add('hidden');
startScreen.classList.remove('visible');
hud.classList.add('visible');
bird.vy = -7.2;
sfx.flap();
}
function gameOver() {
state = STATE.DYING;
sfx.hit();
setTimeout(sfx.die, 120);
shake = 18;
spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
flashScreen();
bird.alive = false;
const isNewBest = score > best;
if (isNewBest) {
best = score;
localStorage.setItem('sunsetFlierBest', String(best));
}
setTimeout(() => {
state = STATE.OVER;
endScore.textContent = score;
endBest.textContent = best;
const medal = getMedal(score);
if (medal) {
medalEl.style.display = 'block';
medalEl.textContent = medal.symbol;
medalEl.style.color = medal.color;
} else {
medalEl.style.display = 'none';
}
endScreen.classList.remove('hidden');
endScreen.classList.add('visible');
if (isNewBest) {
setTimeout(()=>hud.classList.add('has-new-best'), 0);
}
}, 700);
}
function flashScreen() {
flashEl.style.transition = 'none';
flashEl.style.opacity = '0.9';
requestAnimationFrame(() => {
flashEl.style.transition = 'opacity 0.4s ease-out';
flashEl.style.opacity = '0';
});
}
function getMedal(s) {
if (s >= 30) return { symbol: 'β
', color: '#ffd93b' };
if (s >= 20) return { symbol: 'β¦', color: '#c0c0c0' };
if (s >= 10) return { symbol: 'β§', color: '#cd7f32' };
if (s >= 5) return { symbol: 'β’', color: '#ff9ff3' };
return null;
}
function restart() {
reset();
state = STATE.READY;
endScreen.classList.add('hidden');
endScreen.classList.remove('visible');
startScreen.classList.remove('hidden');
startScreen.classList.add('visible');
hud.classList.remove('visible');
hud.classList.remove('has-new-best');
}
// ============ Input ============
function flap() {
if (state === STATE.READY) { startGame(); return; }
if (state === STATE.PLAYING) {
bird.vy = -7.2;
bird.wingPhase = 0;
sfx.flap();
spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
}
if (state === STATE.OVER) {
// tap anywhere on end screen also restarts
// but we let button handle it primarily
}
}
function handleInput(e) {
if (e.target.closest('button')) return;
e.preventDefault();
flap();
}
frame.addEventListener('mousedown', handleInput);
frame.addEventListener('touchstart', handleInput, { passive: false });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
e.preventDefault();
if (state === STATE.OVER) restart();
else flap();
}
});
restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });
// ============ Update ============
const GRAVITY = 0.42;
const MAX_FALL = 9;
function update() {
frameCount++;
if (state === STATE.READY) {
// idle bob
bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
bird.wingPhase += 0.2;
bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
}
if (state === STATE.PLAYING) {
bird.vy += GRAVITY;
if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
bird.y += bird.vy;
bird.wingPhase += 0.4;
bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));
// Pipes
for (const p of pipes) {
p.x -= PIPE_SPEED;
if (!p.scored && p.x + PIPE_W < bird.x) {
p.scored = true;
score++;
hudScore.textContent = score;
sfx.score();
spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
}
}
// Remove off-screen pipes, add new
while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
const last = pipes[pipes.length - 1];
if (last && last.x < W - PIPE_SPACING) {
pipes.push(makePipe(last.x + PIPE_SPACING));
}
// Collision
// Ground & ceiling
const groundY = H - 80;
if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
gameOver();
return;
}
// Pipes
for (const p of pipes) {
if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
const top = p.gapY;
const bottom = p.gapY + PIPE_GAP;
if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
// collision
gameOver();
return;
}
}
}
if (state === STATE.DYING) {
bird.vy += GRAVITY * 1.2;
bird.y += bird.vy;
bird.rotation += 0.06;
const groundY = H - 80;
if (bird.y + bird.radius > groundY) {
bird.y = groundY - bird.radius;
bird.vy = 0;
}
}
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life -= 0.02;
if (p.life <= 0) particles.splice(i, 1);
}
// Shake decay
if (shake > 0) shake = Math.max(0, shake - 1.2);
// Background animations
for (const c of clouds) {
c.x -= c.speed;
if (c.x < -100) c.x = W + 100;
}
for (const s of stars) {
s.tw += 0.05;
}
groundX = (groundX - PIPE_SPEED) % 120;
}
// ============ Render ============
function render() {
ctx.save();
// Shake
if (shake > 0) {
const sx = (Math.random() - 0.5) * shake;
const sy = (Math.random() - 0.5) * shake;
ctx.translate(sx, sy);
}
// Sky gradient
const sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, '#1a1746');
sky.addColorStop(0.3, '#4a3a8e');
sky.addColorStop(0.55, '#ff6b6b');
sky.addColorStop(0.8, '#feca50');
sky.addColorStop(1, '#ffd93b');
ctx.fillStyle = sky;
ctx.fillRect(-20, -20, W + 40, H + 40);
// Stars
for (const s of stars) {
const a = 0.4 + Math.sin(s.tw) * 0.4;
ctx.fillStyle = `rgba(255,255,255,${a})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// Sun
const sunX = W * 0.75;
const sunY = H * 0.65;
const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(sunX - 200, sunY - 200, 400, 400);
ctx.fillStyle = '#fff6d0';
ctx.beginPath();
ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
ctx.fill();
// Clouds
for (const c of clouds) {
drawCloud(c.x, c.y, c.scale);
}
// Mountains silhouette
ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
ctx.beginPath();
ctx.moveTo(0, H * 0.72);
const peaks = [
[W * 0.15, H * 0.58],
[W * 0.25, H * 0.68],
[W * 0.4, H * 0.52],
[W * 0.55, H * 0.66],
[W * 0.7, H * 0.6],
[W * 0.85, H * 0.68],
[W, H * 0.62]
];
for (const [px, py] of peaks) ctx.lineTo(px, py);
ctx.lineTo(W, H);
ctx.lineTo(0, H);
ctx.closePath();
ctx.fill();
// Pipes
for (const p of pipes) {
drawPipe(p);
}
// Ground
drawGround();
// Bird
drawBird();
// Particles
for (const part of particles) {
ctx.fillStyle = part.color;
ctx.globalAlpha = part.life;
ctx.beginPath();
ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawCloud(x, y, scale) {
ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
const s = scale;
ctx.beginPath();
ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
ctx.fill();
}
function drawPipe(p) {
const x = p.x;
const topY = p.gapY;
const bottomY = p.gapY + PIPE_GAP;
const groundY = H - 80;
// gradient
const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
grad.addColorStop(0, '#2d5a1a');
grad.addColorStop(0.15, '#4a902b');
grad.addColorStop(0.5, '#7bc94a');
grad.addColorStop(0.85, '#4a902b');
grad.addColorStop(1, '#2d5a1a');
// Top pipe
ctx.fillStyle = grad;
ctx.fillRect(x, 0, PIPE_W, topY - 18);
// Top cap
ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
// Top cap highlight
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, topY - 18, 14, 18);
// Bottom pipe
ctx.fillStyle = grad;
ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
// Bottom cap
ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, bottomY, 14, 18);
// Outline
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 2;
ctx.strokeRect(x, 0, PIPE_W, topY - 18);
ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
}
function drawGround() {
const groundY = H - 80;
// Sand/dusk ground
const g = ctx.createLinearGradient(0, groundY, 0, H);
g.addColorStop(0, '#8b5a2b');
g.addColorStop(0.4, '#5e3a1c');
g.addColorStop(1, '#2d1a0c');
ctx.fillStyle = g;
ctx.fillRect(0, groundY, W, 80);
// surface line
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(W, groundY);
ctx.stroke();
// Pattern stripes (moving)
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let i = -2; i < W / 40 + 2; i++) {
const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
ctx.beginPath();
ctx.moveTo(stripeX, groundY);
ctx.lineTo(stripeX + 30, groundY + 80);
ctx.lineTo(stripeX + 50, groundY + 80);
ctx.lineTo(stripeX + 20, groundY);
ctx.closePath();
ctx.fill();
}
// Pebbles on top
ctx.fillStyle = '#3d2412';
for (let i = 0; i < 12; i++) {
const px = (i * 60 + groundX + 60) % (W + 40);
ctx.beginPath();
ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawBird() {
const b = bird;
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.rotation);
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
ctx.fill();
// Body
const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
bodyGrad.addColorStop(0, '#ff9ff3');
bodyGrad.addColorStop(0.5, '#ff6b6b');
bodyGrad.addColorStop(1, '#c0334a');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
ctx.fill();
// Wing (flapping)
const wingY = Math.sin(b.wingPhase) * 8;
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1.5;
ctx.stroke();
// Eye
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(8, -4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1a1746';
ctx.beginPath();
ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(10, -5, 1, 0, Math.PI * 2);
ctx.fill();
// Beak
ctx.fillStyle = '#ffaa33';
ctx.beginPath();
ctx.moveTo(13, -1);
ctx.lineTo(20, 1);
ctx.lineTo(13, 3);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1;
ctx.stroke();
// Tail
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.moveTo(-12, -2);
ctx.lineTo(-18, -5);
ctx.lineTo(-16, 0);
ctx.lineTo(-18, 5);
ctx.lineTo(-12, 3);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
// ============ Loop ============
function loop() {
update();
render();
requestAnimationFrame(loop);
}
reset();
loop();
})();
</script>
</body>
</html>
```
Here's what I built:
**Theme**: "Sunset Flier" β instead of the classic green/teal Flappy Bird look, I went with a dusk-sunset palette (deep purple β coral β gold) with silhouetted mountains, a glowing sun, drifting clouds, and twinkling stars in the upper sky.
**Bird**: A coral-pink bird with a yellow flapping wing, animated beak, tail, and tracking eye. Wing flaps physically animate when the bird rises/falls.
**Mechanics**:
- Classic gravity + flap-impulse physics, tuned to feel responsive
- Pipes with random gap placement, smooth scrolling, cap-with-highlight rendering for a chunky 2D look
- Score increments when the bird passes a pipe, with a particle pop and chime SFX
- Collision with pipes, ceiling, and ground triggers game over with screen flash, screen shake, and death particles
**Polish**:
- Press Start 2P pixel-font HUD with score and "NEW BEST!" pulse on record-breaking runs
- Game-over panel with medal system (β
/ β¦ / β§ / β’) based on score thresholds
- High score persisted via `localStorage`
- Animated start screen with bobbing panel
- Procedural sound effects via Web Audio API (flap, score, hit, death) β no external assets
- Moving ground stripes/pebbles to reinforce the sense of speed
- Responsive: scales to viewport, supports mouse, touch, and keyboard (Space/β/Enter)
- Retry button + tap-anywhere-to-restart convenience
Just open the file in a browser β tap/click/press space to fly.
f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1
./llama.cpp/llama-cli \
--model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \
--temp 1.0 \
--top-p 0.95 \
--min-p 0.01 \
--cache-type-k q4_1 \
--cache-type-v q4_1